cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / login / user_pod_row.js
blobed3d8ac25520711d8547147872a1a5fcd5f2414a
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="wallpaper_loader.js"></include>
7 /**
8  * @fileoverview User pod row implementation.
9  */
11 cr.define('login', function() {
12   /**
13    * Number of displayed columns depending on user pod count.
14    * @type {Array.<number>}
15    * @const
16    */
17   var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6];
19   /**
20    * Mapping between number of columns in pod-row and margin between user pods
21    * for such layout.
22    * @type {Array.<number>}
23    * @const
24    */
25   var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12];
27   /**
28    * Maximal number of columns currently supported by pod-row.
29    * @type {number}
30    * @const
31    */
32   var MAX_NUMBER_OF_COLUMNS = 6;
34   /**
35    * Variables used for pod placement processing.
36    * Width and height should be synced with computed CSS sizes of pods.
37    */
38   var POD_WIDTH = 180;
39   var POD_HEIGHT = 217;
40   var POD_ROW_PADDING = 10;
42   /**
43    * Whether to preselect the first pod automatically on login screen.
44    * @type {boolean}
45    * @const
46    */
47   var PRESELECT_FIRST_POD = true;
49   /**
50    * Maximum time for which the pod row remains hidden until all user images
51    * have been loaded.
52    * @type {number}
53    * @const
54    */
55   var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
57   /**
58    * Public session help topic identifier.
59    * @type {number}
60    * @const
61    */
62   var HELP_TOPIC_PUBLIC_SESSION = 3041033;
64   /**
65    * Oauth token status. These must match UserManager::OAuthTokenStatus.
66    * @enum {number}
67    * @const
68    */
69   var OAuthTokenStatus = {
70     UNKNOWN: 0,
71     INVALID_OLD: 1,
72     VALID_OLD: 2,
73     INVALID_NEW: 3,
74     VALID_NEW: 4
75   };
77   /**
78    * Tab order for user pods. Update these when adding new controls.
79    * @enum {number}
80    * @const
81    */
82   var UserPodTabOrder = {
83     POD_INPUT: 1,     // Password input fields (and whole pods themselves).
84     HEADER_BAR: 2,    // Buttons on the header bar (Shutdown, Add User).
85     ACTION_BOX: 3,    // Action box buttons.
86     PAD_MENU_ITEM: 4  // User pad menu items (Remove this user).
87   };
89   // Focus and tab order are organized as follows:
90   //
91   // (1) all user pods have tab index 1 so they are traversed first;
92   // (2) when a user pod is activated, its tab index is set to -1 and its
93   //     main input field gets focus and tab index 1;
94   // (3) buttons on the header bar have tab index 2 so they follow user pods;
95   // (4) Action box buttons have tab index 3 and follow header bar buttons;
96   // (5) lastly, focus jumps to the Status Area and back to user pods.
97   //
98   // 'Focus' event is handled by a capture handler for the whole document
99   // and in some cases 'mousedown' event handlers are used instead of 'click'
100   // handlers where it's necessary to prevent 'focus' event from being fired.
102   /**
103    * Helper function to remove a class from given element.
104    * @param {!HTMLElement} el Element whose class list to change.
105    * @param {string} cl Class to remove.
106    */
107   function removeClass(el, cl) {
108     el.classList.remove(cl);
109   }
111   /**
112    * Creates a user pod.
113    * @constructor
114    * @extends {HTMLDivElement}
115    */
116   var UserPod = cr.ui.define(function() {
117     var node = $('user-pod-template').cloneNode(true);
118     node.removeAttribute('id');
119     return node;
120   });
122   /**
123    * Stops event propagation from the any user pod child element.
124    * @param {Event} e Event to handle.
125    */
126   function stopEventPropagation(e) {
127     // Prevent default so that we don't trigger a 'focus' event.
128     e.preventDefault();
129     e.stopPropagation();
130   }
132   /**
133    * Unique salt added to user image URLs to prevent caching. Dictionary with
134    * user names as keys.
135    * @type {Object}
136    */
137   UserPod.userImageSalt_ = {};
139   UserPod.prototype = {
140     __proto__: HTMLDivElement.prototype,
142     /** @override */
143     decorate: function() {
144       this.tabIndex = UserPodTabOrder.POD_INPUT;
145       this.customButton.tabIndex = UserPodTabOrder.POD_INPUT;
146       this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
148       // Mousedown has to be used instead of click to be able to prevent 'focus'
149       // event later.
150       this.addEventListener('mousedown',
151           this.handleMouseDown_.bind(this));
153       this.signinButtonElement.addEventListener('click',
154           this.activate.bind(this));
156       this.actionBoxAreaElement.addEventListener('mousedown',
157                                                  stopEventPropagation);
158       this.actionBoxAreaElement.addEventListener('click',
159           this.handleActionAreaButtonClick_.bind(this));
160       this.actionBoxAreaElement.addEventListener('keydown',
161           this.handleActionAreaButtonKeyDown_.bind(this));
163       this.actionBoxMenuRemoveElement.addEventListener('click',
164           this.handleRemoveCommandClick_.bind(this));
165       this.actionBoxMenuRemoveElement.addEventListener('keydown',
166           this.handleRemoveCommandKeyDown_.bind(this));
167       this.actionBoxMenuRemoveElement.addEventListener('blur',
168           this.handleRemoveCommandBlur_.bind(this));
170       if (this.actionBoxRemoveUserWarningButtonElement) {
171         this.actionBoxRemoveUserWarningButtonElement.addEventListener(
172             'click',
173             this.handleRemoveUserConfirmationClick_.bind(this));
174       }
176       this.customButton.addEventListener('click',
177           this.handleCustomButtonClick_.bind(this));
178     },
180     /**
181      * Initializes the pod after its properties set and added to a pod row.
182      */
183     initialize: function() {
184       this.passwordElement.addEventListener('keydown',
185           this.parentNode.handleKeyDown.bind(this.parentNode));
186       this.passwordElement.addEventListener('keypress',
187           this.handlePasswordKeyPress_.bind(this));
189       this.imageElement.addEventListener('load',
190           this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
191     },
193     /**
194      * Resets tab order for pod elements to its initial state.
195      */
196     resetTabOrder: function() {
197       this.tabIndex = UserPodTabOrder.POD_INPUT;
198       this.mainInput.tabIndex = -1;
199     },
201     /**
202      * Handles keypress event (i.e. any textual input) on password input.
203      * @param {Event} e Keypress Event object.
204      * @private
205      */
206     handlePasswordKeyPress_: function(e) {
207       // When tabbing from the system tray a tab key press is received. Suppress
208       // this so as not to type a tab character into the password field.
209       if (e.keyCode == 9) {
210         e.preventDefault();
211         return;
212       }
213     },
215     /**
216      * Top edge margin number of pixels.
217      * @type {?number}
218      */
219     set top(top) {
220       this.style.top = cr.ui.toCssPx(top);
221     },
222     /**
223      * Left edge margin number of pixels.
224      * @type {?number}
225      */
226     set left(left) {
227       this.style.left = cr.ui.toCssPx(left);
228     },
230     /**
231      * Gets signed in indicator element.
232      * @type {!HTMLDivElement}
233      */
234     get signedInIndicatorElement() {
235       return this.querySelector('.signed-in-indicator');
236     },
238     /**
239      * Gets image element.
240      * @type {!HTMLImageElement}
241      */
242     get imageElement() {
243       return this.querySelector('.user-image');
244     },
246     /**
247      * Gets name element.
248      * @type {!HTMLDivElement}
249      */
250     get nameElement() {
251       return this.querySelector('.name');
252     },
254     /**
255      * Gets password field.
256      * @type {!HTMLInputElement}
257      */
258     get passwordElement() {
259       return this.querySelector('.password');
260     },
262     /**
263      * Gets Caps Lock hint image.
264      * @type {!HTMLImageElement}
265      */
266     get capslockHintElement() {
267       return this.querySelector('.capslock-hint');
268     },
270     /**
271      * Gets user signin button.
272      * @type {!HTMLInputElement}
273      */
274     get signinButtonElement() {
275       return this.querySelector('.signin-button');
276     },
278     /**
279      * Gets action box area.
280      * @type {!HTMLInputElement}
281      */
282     get actionBoxAreaElement() {
283       return this.querySelector('.action-box-area');
284     },
286     /**
287      * Gets user type icon area.
288      * @type {!HTMLInputElement}
289      */
290     get userTypeIconAreaElement() {
291       return this.querySelector('.user-type-icon-area');
292     },
294     /**
295      * Gets action box menu.
296      * @type {!HTMLInputElement}
297      */
298     get actionBoxMenuElement() {
299       return this.querySelector('.action-box-menu');
300     },
302     /**
303      * Gets action box menu title.
304      * @type {!HTMLInputElement}
305      */
306     get actionBoxMenuTitleElement() {
307       return this.querySelector('.action-box-menu-title');
308     },
310     /**
311      * Gets action box menu title, user name item.
312      * @type {!HTMLInputElement}
313      */
314     get actionBoxMenuTitleNameElement() {
315       return this.querySelector('.action-box-menu-title-name');
316     },
318     /**
319      * Gets action box menu title, user email item.
320      * @type {!HTMLInputElement}
321      */
322     get actionBoxMenuTitleEmailElement() {
323       return this.querySelector('.action-box-menu-title-email');
324     },
326     /**
327      * Gets action box menu, remove user command item.
328      * @type {!HTMLInputElement}
329      */
330     get actionBoxMenuCommandElement() {
331       return this.querySelector('.action-box-menu-remove-command');
332     },
334     /**
335      * Gets action box menu, remove user command item div.
336      * @type {!HTMLInputElement}
337      */
338     get actionBoxMenuRemoveElement() {
339       return this.querySelector('.action-box-menu-remove');
340     },
342     /**
343      * Gets action box menu, remove user command item div.
344      * @type {!HTMLInputElement}
345      */
346     get actionBoxRemoveUserWarningElement() {
347       return this.querySelector('.action-box-remove-user-warning');
348     },
350     /**
351      * Gets action box menu, remove user command item div.
352      * @type {!HTMLInputElement}
353      */
354     get actionBoxRemoveUserWarningButtonElement() {
355       return this.querySelector(
356           '.remove-warning-button');
357     },
359     /**
360      * Gets the locked user indicator box.
361      * @type {!HTMLInputElement}
362      */
363     get lockedIndicatorElement() {
364       return this.querySelector('.locked-indicator');
365     },
367     /**
368      * Gets the custom button. This button is normally hidden, but can be
369      * shown using the chrome.screenlockPrivate API.
370      * @type {!HTMLInputElement}
371      */
372     get customButton() {
373       return this.querySelector('.custom-button');
374     },
376     /**
377      * Updates the user pod element.
378      */
379     update: function() {
380       this.imageElement.src = 'chrome://userimage/' + this.user.username +
381           '?id=' + UserPod.userImageSalt_[this.user.username];
383       this.nameElement.textContent = this.user_.displayName;
384       this.signedInIndicatorElement.hidden = !this.user_.signedIn;
386       var needSignin = this.needSignin;
387       this.passwordElement.hidden = needSignin;
388       this.signinButtonElement.hidden = !needSignin;
390       this.updateActionBoxArea();
391     },
393     updateActionBoxArea: function() {
394       this.actionBoxAreaElement.hidden = this.user_.publicAccount;
395       this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
397       this.actionBoxAreaElement.setAttribute(
398           'aria-label', loadTimeData.getStringF(
399               'podMenuButtonAccessibleName', this.user_.emailAddress));
400       this.actionBoxMenuRemoveElement.setAttribute(
401           'aria-label', loadTimeData.getString(
402                'podMenuRemoveItemAccessibleName'));
403       this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
404           loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
405           this.user_.displayName;
406       this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
407       this.actionBoxMenuTitleEmailElement.hidden =
408           this.user_.locallyManagedUser;
410       this.actionBoxMenuCommandElement.textContent =
411           loadTimeData.getString('removeUser');
412       this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
413           'passwordFieldAccessibleName', this.user_.emailAddress));
414       this.userTypeIconAreaElement.hidden = !this.user_.locallyManagedUser;
415     },
417     /**
418      * The user that this pod represents.
419      * @type {!Object}
420      */
421     user_: undefined,
422     get user() {
423       return this.user_;
424     },
425     set user(userDict) {
426       this.user_ = userDict;
427       this.update();
428     },
430     /**
431      * Whether signin is required for this user.
432      */
433     get needSignin() {
434       // Signin is performed if the user has an invalid oauth token and is
435       // not currently signed in (i.e. not the lock screen).
436       return this.user.oauthTokenStatus != OAuthTokenStatus.VALID_OLD &&
437           this.user.oauthTokenStatus != OAuthTokenStatus.VALID_NEW &&
438           !this.user.signedIn;
439     },
441     /**
442      * Gets main input element.
443      * @type {(HTMLButtonElement|HTMLInputElement)}
444      */
445     get mainInput() {
446       if (!this.signinButtonElement.hidden)
447         return this.signinButtonElement;
448       else
449         return this.passwordElement;
450     },
452     /**
453      * Whether action box button is in active state.
454      * @type {boolean}
455      */
456     get isActionBoxMenuActive() {
457       return this.actionBoxAreaElement.classList.contains('active');
458     },
459     set isActionBoxMenuActive(active) {
460       if (active == this.isActionBoxMenuActive)
461         return;
463       if (active) {
464         this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
465         if (this.actionBoxRemoveUserWarningElement)
466           this.actionBoxRemoveUserWarningElement.hidden = true;
468         // Clear focus first if another pod is focused.
469         if (!this.parentNode.isFocused(this)) {
470           this.parentNode.focusPod(undefined, true);
471           this.actionBoxAreaElement.focus();
472         }
473         this.actionBoxAreaElement.classList.add('active');
474       } else {
475         this.actionBoxAreaElement.classList.remove('active');
476       }
477     },
479     /**
480      * Whether action box button is in hovered state.
481      * @type {boolean}
482      */
483     get isActionBoxMenuHovered() {
484       return this.actionBoxAreaElement.classList.contains('hovered');
485     },
486     set isActionBoxMenuHovered(hovered) {
487       if (hovered == this.isActionBoxMenuHovered)
488         return;
490       if (hovered) {
491         this.actionBoxAreaElement.classList.add('hovered');
492         this.classList.add('hovered');
493       } else {
494         this.actionBoxAreaElement.classList.remove('hovered');
495         this.classList.remove('hovered');
496       }
497     },
499     /**
500      * Updates the image element of the user.
501      */
502     updateUserImage: function() {
503       UserPod.userImageSalt_[this.user.username] = new Date().getTime();
504       this.update();
505     },
507     /**
508      * Focuses on input element.
509      */
510     focusInput: function() {
511       var needSignin = this.needSignin;
512       this.signinButtonElement.hidden = !needSignin;
513       this.passwordElement.hidden = needSignin;
515       // Move tabIndex from the whole pod to the main input.
516       this.tabIndex = -1;
517       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
518       this.mainInput.focus();
519     },
521     /**
522      * Activates the pod.
523      * @return {boolean} True if activated successfully.
524      */
525     activate: function() {
526       if (!this.signinButtonElement.hidden) {
527         this.showSigninUI();
528       } else if (!this.passwordElement.value) {
529         return false;
530       } else {
531         Oobe.disableSigninUI();
532         chrome.send('authenticateUser',
533                     [this.user.username, this.passwordElement.value]);
534       }
536       return true;
537     },
539     showSupervisedUserSigninWarning: function() {
540       // Locally managed user token has been invalidated.
541       // Make sure that pod is focused i.e. "Sign in" button is seen.
542       this.parentNode.focusPod(this);
544       var error = document.createElement('div');
545       var messageDiv = document.createElement('div');
546       messageDiv.className = 'error-message-bubble';
547       messageDiv.textContent =
548           loadTimeData.getString('supervisedUserExpiredTokenWarning');
549       error.appendChild(messageDiv);
551       $('bubble').showContentForElement(
552           this.signinButtonElement,
553           cr.ui.Bubble.Attachment.TOP,
554           error,
555           this.signinButtonElement.offsetWidth / 2,
556           4);
557     },
559     /**
560      * Shows signin UI for this user.
561      */
562     showSigninUI: function() {
563       if (this.user.locallyManagedUser) {
564         this.showSupervisedUserSigninWarning();
565       } else {
566         this.parentNode.showSigninUI(this.user.emailAddress);
567       }
568     },
570     /**
571      * Resets the input field and updates the tab order of pod controls.
572      * @param {boolean} takeFocus If true, input field takes focus.
573      */
574     reset: function(takeFocus) {
575       this.passwordElement.value = '';
576       if (takeFocus)
577         this.focusInput();  // This will set a custom tab order.
578       else
579         this.resetTabOrder();
580     },
582     /**
583      * Handles a click event on action area button.
584      * @param {Event} e Click event.
585      */
586     handleActionAreaButtonClick_: function(e) {
587       if (this.parentNode.disabled)
588         return;
589       this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
590     },
592     /**
593      * Handles a keydown event on action area button.
594      * @param {Event} e KeyDown event.
595      */
596     handleActionAreaButtonKeyDown_: function(e) {
597       if (this.disabled)
598         return;
599       switch (e.keyIdentifier) {
600         case 'Enter':
601         case 'U+0020':  // Space
602           if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
603             this.isActionBoxMenuActive = true;
604           e.stopPropagation();
605           break;
606         case 'Up':
607         case 'Down':
608           if (this.isActionBoxMenuActive) {
609             this.actionBoxMenuRemoveElement.tabIndex =
610                 UserPodTabOrder.PAD_MENU_ITEM;
611             this.actionBoxMenuRemoveElement.focus();
612           }
613           e.stopPropagation();
614           break;
615         case 'U+001B':  // Esc
616           this.isActionBoxMenuActive = false;
617           e.stopPropagation();
618           break;
619         case 'U+0009':  // Tab
620           this.parentNode.focusPod();
621         default:
622           this.isActionBoxMenuActive = false;
623           break;
624       }
625     },
627     /**
628      * Handles a click event on remove user command.
629      * @param {Event} e Click event.
630      */
631     handleRemoveCommandClick_: function(e) {
632       if (this.user.locallyManagedUser || this.user.isDesktopUser) {
633         this.showRemoveWarning_();
634         return;
635       }
636       if (this.isActionBoxMenuActive)
637         chrome.send('removeUser', [this.user.username]);
638     },
640     /**
641      * Shows remove warning for managed users.
642      */
643     showRemoveWarning_: function() {
644       this.actionBoxMenuRemoveElement.hidden = true;
645       this.actionBoxRemoveUserWarningElement.hidden = false;
646     },
648     /**
649      * Handles a click event on remove user confirmation button.
650      * @param {Event} e Click event.
651      */
652     handleRemoveUserConfirmationClick_: function(e) {
653       if (this.isActionBoxMenuActive)
654         chrome.send('removeUser', [this.user.username]);
655     },
657     /**
658      * Handles a keydown event on remove command.
659      * @param {Event} e KeyDown event.
660      */
661     handleRemoveCommandKeyDown_: function(e) {
662       if (this.disabled)
663         return;
664       switch (e.keyIdentifier) {
665         case 'Enter':
666           chrome.send('removeUser', [this.user.username]);
667           e.stopPropagation();
668           break;
669         case 'Up':
670         case 'Down':
671           e.stopPropagation();
672           break;
673         case 'U+001B':  // Esc
674           this.actionBoxAreaElement.focus();
675           this.isActionBoxMenuActive = false;
676           e.stopPropagation();
677           break;
678         default:
679           this.actionBoxAreaElement.focus();
680           this.isActionBoxMenuActive = false;
681           break;
682       }
683     },
685     /**
686      * Handles a blur event on remove command.
687      * @param {Event} e Blur event.
688      */
689     handleRemoveCommandBlur_: function(e) {
690       if (this.disabled)
691         return;
692       this.actionBoxMenuRemoveElement.tabIndex = -1;
693     },
695     /**
696      * Handles mousedown event on a user pod.
697      * @param {Event} e Mousedown event.
698      */
699     handleMouseDown_: function(e) {
700       if (this.parentNode.disabled)
701         return;
703       if (!this.signinButtonElement.hidden && !this.isActionBoxMenuActive) {
704         this.showSigninUI();
705         // Prevent default so that we don't trigger 'focus' event.
706         e.preventDefault();
707       }
708     },
710     /**
711      * Called when the custom button is clicked.
712      */
713     handleCustomButtonClick_: function() {
714       chrome.send('customButtonClicked', [this.user.username]);
715     }
716   };
718   /**
719    * Creates a public account user pod.
720    * @constructor
721    * @extends {UserPod}
722    */
723   var PublicAccountUserPod = cr.ui.define(function() {
724     var node = UserPod();
726     var extras = $('public-account-user-pod-extras-template').children;
727     for (var i = 0; i < extras.length; ++i) {
728       var el = extras[i].cloneNode(true);
729       node.appendChild(el);
730     }
732     return node;
733   });
735   PublicAccountUserPod.prototype = {
736     __proto__: UserPod.prototype,
738     /**
739      * "Enter" button in expanded side pane.
740      * @type {!HTMLButtonElement}
741      */
742     get enterButtonElement() {
743       return this.querySelector('.enter-button');
744     },
746     /**
747      * Boolean flag of whether the pod is showing the side pane. The flag
748      * controls whether 'expanded' class is added to the pod's class list and
749      * resets tab order because main input element changes when the 'expanded'
750      * state changes.
751      * @type {boolean}
752      */
753     get expanded() {
754       return this.classList.contains('expanded');
755     },
756     set expanded(expanded) {
757       if (this.expanded == expanded)
758         return;
760       this.resetTabOrder();
761       this.classList.toggle('expanded', expanded);
763       var self = this;
764       this.classList.add('animating');
765       this.addEventListener('webkitTransitionEnd', function f(e) {
766         self.removeEventListener('webkitTransitionEnd', f);
767         self.classList.remove('animating');
769         // Accessibility focus indicator does not move with the focused
770         // element. Sends a 'focus' event on the currently focused element
771         // so that accessibility focus indicator updates its location.
772         if (document.activeElement)
773           document.activeElement.dispatchEvent(new Event('focus'));
774       });
775     },
777     /** @override */
778     get needSignin() {
779       return false;
780     },
782     /** @override */
783     get mainInput() {
784       if (this.expanded)
785         return this.enterButtonElement;
786       else
787         return this.nameElement;
788     },
790     /** @override */
791     decorate: function() {
792       UserPod.prototype.decorate.call(this);
794       this.classList.remove('need-password');
795       this.classList.add('public-account');
797       this.nameElement.addEventListener('keydown', (function(e) {
798         if (e.keyIdentifier == 'Enter') {
799           this.parentNode.activatedPod = this;
800           // Stop this keydown event from bubbling up to PodRow handler.
801           e.stopPropagation();
802           // Prevent default so that we don't trigger a 'click' event on the
803           // newly focused "Enter" button.
804           e.preventDefault();
805         }
806       }).bind(this));
808       var learnMore = this.querySelector('.learn-more');
809       learnMore.addEventListener('mousedown', stopEventPropagation);
810       learnMore.addEventListener('click', this.handleLearnMoreEvent);
811       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
813       learnMore = this.querySelector('.side-pane-learn-more');
814       learnMore.addEventListener('click', this.handleLearnMoreEvent);
815       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
817       this.enterButtonElement.addEventListener('click', (function(e) {
818         this.enterButtonElement.disabled = true;
819         chrome.send('launchPublicAccount', [this.user.username]);
820       }).bind(this));
821     },
823     /**
824      * Updates the user pod element.
825      */
826     update: function() {
827       UserPod.prototype.update.call(this);
828       this.querySelector('.side-pane-name').textContent =
829           this.user_.displayName;
830       this.querySelector('.info').textContent =
831           loadTimeData.getStringF('publicAccountInfoFormat',
832                                   this.user_.enterpriseDomain);
833     },
835     /** @override */
836     focusInput: function() {
837       // Move tabIndex from the whole pod to the main input.
838       this.tabIndex = -1;
839       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
840       this.mainInput.focus();
841     },
843     /** @override */
844     reset: function(takeFocus) {
845       if (!takeFocus)
846         this.expanded = false;
847       this.enterButtonElement.disabled = false;
848       UserPod.prototype.reset.call(this, takeFocus);
849     },
851     /** @override */
852     activate: function() {
853       this.expanded = true;
854       this.focusInput();
855       return true;
856     },
858     /** @override */
859     handleMouseDown_: function(e) {
860       if (this.parentNode.disabled)
861         return;
863       this.parentNode.focusPod(this);
864       this.parentNode.activatedPod = this;
865       // Prevent default so that we don't trigger 'focus' event.
866       e.preventDefault();
867     },
869     /**
870      * Handle mouse and keyboard events for the learn more button.
871      * Triggering the button causes information about public sessions to be
872      * shown.
873      * @param {Event} event Mouse or keyboard event.
874      */
875     handleLearnMoreEvent: function(event) {
876       switch (event.type) {
877         // Show informaton on left click. Let any other clicks propagate.
878         case 'click':
879           if (event.button != 0)
880             return;
881           break;
882         // Show informaton when <Return> or <Space> is pressed. Let any other
883         // key presses propagate.
884         case 'keydown':
885           switch (event.keyCode) {
886             case 13:  // Return.
887             case 32:  // Space.
888               break;
889             default:
890               return;
891           }
892           break;
893       }
894       chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
895       stopEventPropagation(event);
896     },
897   };
899   /**
900    * Creates a user pod to be used only in desktop chrome.
901    * @constructor
902    * @extends {UserPod}
903    */
904   var DesktopUserPod = cr.ui.define(function() {
905     // Don't just instantiate a UserPod(), as this will call decorate() on the
906     // parent object, and add duplicate event listeners.
907     var node = $('user-pod-template').cloneNode(true);
908     node.removeAttribute('id');
909     return node;
910   });
912   DesktopUserPod.prototype = {
913     __proto__: UserPod.prototype,
915     /** @override */
916     get mainInput() {
917       if (!this.passwordElement.hidden)
918         return this.passwordElement;
919       else
920         return this.nameElement;
921     },
923     /** @override */
924     decorate: function() {
925       UserPod.prototype.decorate.call(this);
926     },
928     /** @override */
929     update: function() {
930       // TODO(noms): Use the actual profile avatar for local profiles once the
931       // new, non-pixellated avatars are available.
932       this.imageElement.src = this.user.emailAddress == '' ?
933           'chrome://theme/IDR_USER_MANAGER_DEFAULT_AVATAR' :
934           this.user.userImage;
935       this.nameElement.textContent = this.user.displayName;
937       var isLockedUser = this.user.needsSignin;
938       this.signinButtonElement.hidden = true;
939       this.lockedIndicatorElement.hidden = !isLockedUser;
940       this.passwordElement.hidden = !isLockedUser;
941       this.nameElement.hidden = isLockedUser;
943       UserPod.prototype.updateActionBoxArea.call(this);
944     },
946     /** @override */
947     focusInput: function() {
948       // For focused pods, display the name unless the pod is locked.
949       var isLockedUser = this.user.needsSignin;
950       this.signinButtonElement.hidden = true;
951       this.lockedIndicatorElement.hidden = !isLockedUser;
952       this.passwordElement.hidden = !isLockedUser;
953       this.nameElement.hidden = isLockedUser;
955       // Move tabIndex from the whole pod to the main input.
956       this.tabIndex = -1;
957       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
958       this.mainInput.focus();
959     },
961     /** @override */
962     reset: function(takeFocus) {
963       // Always display the user's name for unfocused pods.
964       if (!takeFocus)
965         this.nameElement.hidden = false;
966       UserPod.prototype.reset.call(this, takeFocus);
967     },
969     /** @override */
970     activate: function() {
971       if (this.passwordElement.hidden) {
972         Oobe.launchUser(this.user.emailAddress, this.user.displayName);
973       } else if (!this.passwordElement.value) {
974         return false;
975       } else {
976         chrome.send('authenticatedLaunchUser',
977                     [this.user.emailAddress,
978                      this.user.displayName,
979                      this.passwordElement.value]);
980       }
981       this.passwordElement.value = '';
982       return true;
983     },
985     /** @override */
986     handleMouseDown_: function(e) {
987       if (this.parentNode.disabled)
988         return;
990       Oobe.clearErrors();
991       this.parentNode.lastFocusedPod_ = this;
993       // If this is an unlocked pod, then open a browser window. Otherwise
994       // just activate the pod and show the password field.
995       if (!this.user.needsSignin && !this.isActionBoxMenuActive)
996         this.activate();
997     },
999     /** @override */
1000     handleRemoveUserConfirmationClick_: function(e) {
1001       chrome.send('removeUser', [this.user.profilePath]);
1002     },
1003   };
1005   /**
1006    * Creates a new pod row element.
1007    * @constructor
1008    * @extends {HTMLDivElement}
1009    */
1010   var PodRow = cr.ui.define('podrow');
1012   PodRow.prototype = {
1013     __proto__: HTMLDivElement.prototype,
1015     // Whether this user pod row is shown for the first time.
1016     firstShown_: true,
1018     // True if inside focusPod().
1019     insideFocusPod_: false,
1021     // Focused pod.
1022     focusedPod_: undefined,
1024     // Activated pod, i.e. the pod of current login attempt.
1025     activatedPod_: undefined,
1027     // Pod that was most recently focused, if any.
1028     lastFocusedPod_: undefined,
1030     // Note: created only in decorate() !
1031     wallpaperLoader_: undefined,
1033     // Pods whose initial images haven't been loaded yet.
1034     podsWithPendingImages_: [],
1036     /** @override */
1037     decorate: function() {
1038       // Event listeners that are installed for the time period during which
1039       // the element is visible.
1040       this.listeners_ = {
1041         focus: [this.handleFocus_.bind(this), true /* useCapture */],
1042         click: [this.handleClick_.bind(this), true],
1043         mousemove: [this.handleMouseMove_.bind(this), false],
1044         keydown: [this.handleKeyDown.bind(this), false]
1045       };
1046       this.wallpaperLoader_ = new login.WallpaperLoader();
1047     },
1049     /**
1050      * Returns all the pods in this pod row.
1051      * @type {NodeList}
1052      */
1053     get pods() {
1054       return Array.prototype.slice.call(this.children);
1055     },
1057     /**
1058      * Return true if user pod row has only single user pod in it.
1059      * @type {boolean}
1060      */
1061     get isSinglePod() {
1062       return this.children.length == 1;
1063     },
1065     /**
1066      * Returns pod with the given username (null if there is no such pod).
1067      * @param {string} username Username to be matched.
1068      * @return {Object} Pod with the given username. null if pod hasn't been
1069      *                  found.
1070      */
1071     getPodWithUsername_: function(username) {
1072       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1073         if (pod.user.username == username)
1074           return pod;
1075       }
1076       return null;
1077     },
1079     /**
1080      * True if the the pod row is disabled (handles no user interaction).
1081      * @type {boolean}
1082      */
1083     disabled_: false,
1084     get disabled() {
1085       return this.disabled_;
1086     },
1087     set disabled(value) {
1088       this.disabled_ = value;
1089       var controls = this.querySelectorAll('button,input');
1090       for (var i = 0, control; control = controls[i]; ++i) {
1091         control.disabled = value;
1092       }
1093     },
1095     /**
1096      * Creates a user pod from given email.
1097      * @param {string} email User's email.
1098      */
1099     createUserPod: function(user) {
1100       var userPod;
1101       if (user.isDesktopUser)
1102         userPod = new DesktopUserPod({user: user});
1103       else if (user.publicAccount)
1104         userPod = new PublicAccountUserPod({user: user});
1105       else
1106         userPod = new UserPod({user: user});
1108       userPod.hidden = false;
1109       return userPod;
1110     },
1112     /**
1113      * Add an existing user pod to this pod row.
1114      * @param {!Object} user User info dictionary.
1115      * @param {boolean} animated Whether to use init animation.
1116      */
1117     addUserPod: function(user, animated) {
1118       var userPod = this.createUserPod(user);
1119       if (animated) {
1120         userPod.classList.add('init');
1121         userPod.nameElement.classList.add('init');
1122       }
1124       this.appendChild(userPod);
1125       userPod.initialize();
1126     },
1128     /**
1129      * Removes user pod from pod row.
1130      * @param {string} email User's email.
1131      */
1132     removeUserPod: function(username) {
1133       var podToRemove = this.getPodWithUsername_(username);
1134       if (podToRemove == null) {
1135         console.warn('Attempt to remove not existing pod for ' + username +
1136             '.');
1137         return;
1138       }
1139       this.removeChild(podToRemove);
1140       this.placePods_();
1141     },
1143     /**
1144      * Returns index of given pod or -1 if not found.
1145      * @param {UserPod} pod Pod to look up.
1146      * @private
1147      */
1148     indexOf_: function(pod) {
1149       for (var i = 0; i < this.pods.length; ++i) {
1150         if (pod == this.pods[i])
1151           return i;
1152       }
1153       return -1;
1154     },
1156     /**
1157      * Start first time show animation.
1158      */
1159     startInitAnimation: function() {
1160       // Schedule init animation.
1161       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1162         window.setTimeout(removeClass, 500 + i * 70, pod, 'init');
1163         window.setTimeout(removeClass, 700 + i * 70, pod.nameElement, 'init');
1164       }
1165     },
1167     /**
1168      * Start login success animation.
1169      */
1170     startAuthenticatedAnimation: function() {
1171       var activated = this.indexOf_(this.activatedPod_);
1172       if (activated == -1)
1173         return;
1175       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1176         if (i < activated)
1177           pod.classList.add('left');
1178         else if (i > activated)
1179           pod.classList.add('right');
1180         else
1181           pod.classList.add('zoom');
1182       }
1183     },
1185     /**
1186      * Populates pod row with given existing users and start init animation.
1187      * @param {array} users Array of existing user emails.
1188      * @param {boolean} animated Whether to use init animation.
1189      */
1190     loadPods: function(users, animated) {
1191       // Clear existing pods.
1192       this.innerHTML = '';
1193       this.focusedPod_ = undefined;
1194       this.activatedPod_ = undefined;
1195       this.lastFocusedPod_ = undefined;
1197       // Switch off animation
1198       Oobe.getInstance().toggleClass('flying-pods', false);
1200       // Populate the pod row.
1201       for (var i = 0; i < users.length; ++i) {
1202         this.addUserPod(users[i], animated);
1203       }
1204       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1205         this.podsWithPendingImages_.push(pod);
1206       }
1207       // Make sure we eventually show the pod row, even if some image is stuck.
1208       setTimeout(function() {
1209         $('pod-row').classList.remove('images-loading');
1210       }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
1212       this.placePods_();
1214       // Without timeout changes in pods positions will be animated even though
1215       // it happened when 'flying-pods' class was disabled.
1216       setTimeout(function() {
1217         Oobe.getInstance().toggleClass('flying-pods', true);
1218       }, 0);
1220       this.focusPod(this.preselectedPod);
1221     },
1223     /**
1224      * Shows a button on a user pod with an icon. Clicking on this button
1225      * triggers an event used by the chrome.screenlockPrivate API.
1226      * @param {string} username Username of pod to add button
1227      * @param {string} iconURL URL of the button icon
1228      */
1229     showUserPodButton: function(username, iconURL) {
1230       var pod = this.getPodWithUsername_(username);
1231       if (pod == null) {
1232         console.error('Unable to show user pod button for ' + username +
1233                       ': user pod not found.');
1234         return;
1235       }
1237       pod.customButton.hidden = false;
1238       var icon =
1239           pod.customButton.querySelector('.custom-button-icon');
1240       icon.src = iconURL;
1241     },
1243     /**
1244      * Called when window was resized.
1245      */
1246     onWindowResize: function() {
1247       var layout = this.calculateLayout_();
1248       if (layout.columns != this.columns || layout.rows != this.rows)
1249         this.placePods_();
1250     },
1252     /**
1253      * Returns width of podrow having |columns| number of columns.
1254      * @private
1255      */
1256     columnsToWidth_: function(columns) {
1257       var margin = MARGIN_BY_COLUMNS[columns];
1258       return 2 * POD_ROW_PADDING + columns * POD_WIDTH + (columns - 1) * margin;
1259     },
1261     /**
1262      * Returns height of podrow having |rows| number of rows.
1263      * @private
1264      */
1265     rowsToHeight_: function(rows) {
1266       return 2 * POD_ROW_PADDING + rows * POD_HEIGHT;
1267     },
1269     /**
1270      * Calculates number of columns and rows that podrow should have in order to
1271      * hold as much its pods as possible for current screen size. Also it tries
1272      * to choose layout that looks good.
1273      * @return {{columns: number, rows: number}}
1274      */
1275     calculateLayout_: function() {
1276       var preferredColumns = this.pods.length < COLUMNS.length ?
1277           COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
1278       var maxWidth = Oobe.getInstance().clientAreaSize.width;
1279       var columns = preferredColumns;
1280       while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
1281         --columns;
1282       var rows = Math.floor((this.pods.length - 1) / columns) + 1;
1283       var maxHeigth = Oobe.getInstance().clientAreaSize.height;
1284       while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
1285         --rows;
1286       // One more iteration if it's not enough cells to place all pods.
1287       while (maxWidth >= this.columnsToWidth_(columns + 1) &&
1288              columns * rows < this.pods.length &&
1289              columns < MAX_NUMBER_OF_COLUMNS) {
1290          ++columns;
1291       }
1292       return {columns: columns, rows: rows};
1293     },
1295     /**
1296      * Places pods onto their positions onto pod grid.
1297      * @private
1298      */
1299     placePods_: function() {
1300       var layout = this.calculateLayout_();
1301       var columns = this.columns = layout.columns;
1302       var rows = this.rows = layout.rows;
1303       var maxPodsNumber = columns * rows;
1304       var margin = MARGIN_BY_COLUMNS[columns];
1305       this.parentNode.setPreferredSize(
1306           this.columnsToWidth_(columns), this.rowsToHeight_(rows));
1307       this.pods.forEach(function(pod, index) {
1308         if (pod.offsetHeight != POD_HEIGHT)
1309           console.error('Pod offsetHeight and POD_HEIGHT are not equal.');
1310         if (pod.offsetWidth != POD_WIDTH)
1311           console.error('Pod offsetWidht and POD_WIDTH are not equal.');
1312         if (index >= maxPodsNumber) {
1313            pod.hidden = true;
1314            return;
1315         }
1316         pod.hidden = false;
1317         var column = index % columns;
1318         var row = Math.floor(index / columns);
1319         pod.left = POD_ROW_PADDING + column * (POD_WIDTH + margin);
1320         pod.top = POD_ROW_PADDING + row * POD_HEIGHT;
1321       });
1322       Oobe.getInstance().updateScreenSize(this.parentNode);
1323     },
1325     /**
1326      * Number of columns.
1327      * @type {?number}
1328      */
1329     set columns(columns) {
1330       // Cannot use 'columns' here.
1331       this.setAttribute('ncolumns', columns);
1332     },
1333     get columns() {
1334       return this.getAttribute('ncolumns');
1335     },
1337     /**
1338      * Number of rows.
1339      * @type {?number}
1340      */
1341     set rows(rows) {
1342       // Cannot use 'rows' here.
1343       this.setAttribute('nrows', rows);
1344     },
1345     get rows() {
1346       return this.getAttribute('nrows');
1347     },
1349     /**
1350      * Whether the pod is currently focused.
1351      * @param {UserPod} pod Pod to check for focus.
1352      * @return {boolean} Pod focus status.
1353      */
1354     isFocused: function(pod) {
1355       return this.focusedPod_ == pod;
1356     },
1358     /**
1359      * Focuses a given user pod or clear focus when given null.
1360      * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
1361      * @param {boolean=} opt_force If true, forces focus update even when
1362      *                             podToFocus is already focused.
1363      */
1364     focusPod: function(podToFocus, opt_force) {
1365       if (this.isFocused(podToFocus) && !opt_force) {
1366         this.keyboardActivated_ = false;
1367         return;
1368       }
1370       // Make sure there's only one focusPod operation happening at a time.
1371       if (this.insideFocusPod_) {
1372         this.keyboardActivated_ = false;
1373         return;
1374       }
1375       this.insideFocusPod_ = true;
1377       this.wallpaperLoader_.reset();
1378       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1379         if (!this.isSinglePod) {
1380           pod.isActionBoxMenuActive = false;
1381         }
1382         if (pod != podToFocus) {
1383           pod.isActionBoxMenuHovered = false;
1384           pod.classList.remove('focused');
1385           pod.classList.remove('faded');
1386           pod.reset(false);
1387         }
1388       }
1390       // Clear any error messages for previous pod.
1391       if (!this.isFocused(podToFocus))
1392         Oobe.clearErrors();
1394       var hadFocus = !!this.focusedPod_;
1395       this.focusedPod_ = podToFocus;
1396       if (podToFocus) {
1397         podToFocus.classList.remove('faded');
1398         podToFocus.classList.add('focused');
1399         podToFocus.reset(true);  // Reset and give focus.
1400         chrome.send('focusPod', [podToFocus.user.username]);
1402         this.wallpaperLoader_.scheduleLoad(podToFocus.user.username);
1403         this.firstShown_ = false;
1404         this.lastFocusedPod_ = podToFocus;
1405       }
1406       this.insideFocusPod_ = false;
1407       this.keyboardActivated_ = false;
1408     },
1410     /**
1411      * Focuses a given user pod by index or clear focus when given null.
1412      * @param {int=} podToFocus index of User pod to focus.
1413      * @param {boolean=} opt_force If true, forces focus update even when
1414      *                             podToFocus is already focused.
1415      */
1416     focusPodByIndex: function(podToFocus, opt_force) {
1417       if (podToFocus < this.pods.length)
1418         this.focusPod(this.pods[podToFocus], opt_force);
1419     },
1421     /**
1422      * Resets wallpaper to the last active user's wallpaper, if any.
1423      */
1424     loadLastWallpaper: function() {
1425       if (this.lastFocusedPod_)
1426         this.wallpaperLoader_.scheduleLoad(this.lastFocusedPod_.user.username);
1427     },
1429     /**
1430      * Handles 'onWallpaperLoaded' event. Recalculates statistics and
1431      * [re]schedules next wallpaper load.
1432      */
1433     onWallpaperLoaded: function(username) {
1434       this.wallpaperLoader_.onWallpaperLoaded(username);
1435     },
1437     /**
1438      * Returns the currently activated pod.
1439      * @type {UserPod}
1440      */
1441     get activatedPod() {
1442       return this.activatedPod_;
1443     },
1444     set activatedPod(pod) {
1445       if (pod && pod.activate())
1446         this.activatedPod_ = pod;
1447     },
1449     /**
1450      * The pod of the signed-in user, if any; null otherwise.
1451      * @type {?UserPod}
1452      */
1453     get lockedPod() {
1454       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1455         if (pod.user.signedIn)
1456           return pod;
1457       }
1458       return null;
1459     },
1461     /**
1462      * The pod that is preselected on user pod row show.
1463      * @type {?UserPod}
1464      */
1465     get preselectedPod() {
1466       var lockedPod = this.lockedPod;
1467       var preselectedPod = PRESELECT_FIRST_POD ?
1468           lockedPod || this.pods[0] : lockedPod;
1469       return preselectedPod;
1470     },
1472     /**
1473      * Resets input UI.
1474      * @param {boolean} takeFocus True to take focus.
1475      */
1476     reset: function(takeFocus) {
1477       this.disabled = false;
1478       if (this.activatedPod_)
1479         this.activatedPod_.reset(takeFocus);
1480     },
1482     /**
1483      * Restores input focus to current selected pod, if there is any.
1484      */
1485     refocusCurrentPod: function() {
1486       if (this.focusedPod_) {
1487         this.focusedPod_.focusInput();
1488       }
1489     },
1491     /**
1492      * Clears focused pod password field.
1493      */
1494     clearFocusedPod: function() {
1495       if (!this.disabled && this.focusedPod_)
1496         this.focusedPod_.reset(true);
1497     },
1499     /**
1500      * Shows signin UI.
1501      * @param {string} email Email for signin UI.
1502      */
1503     showSigninUI: function(email) {
1504       // Clear any error messages that might still be around.
1505       Oobe.clearErrors();
1506       this.disabled = true;
1507       this.lastFocusedPod_ = this.getPodWithUsername_(email);
1508       Oobe.showSigninUI(email);
1509     },
1511     /**
1512      * Updates current image of a user.
1513      * @param {string} username User for which to update the image.
1514      */
1515     updateUserImage: function(username) {
1516       var pod = this.getPodWithUsername_(username);
1517       if (pod)
1518         pod.updateUserImage();
1519     },
1521     /**
1522      * Resets OAuth token status (invalidates it).
1523      * @param {string} username User for which to reset the status.
1524      */
1525     resetUserOAuthTokenStatus: function(username) {
1526       var pod = this.getPodWithUsername_(username);
1527       if (pod) {
1528         pod.user.oauthTokenStatus = OAuthTokenStatus.INVALID_OLD;
1529         pod.update();
1530       } else {
1531         console.log('Failed to update Gaia state for: ' + username);
1532       }
1533     },
1535     /**
1536      * Handler of click event.
1537      * @param {Event} e Click Event object.
1538      * @private
1539      */
1540     handleClick_: function(e) {
1541       if (this.disabled)
1542         return;
1544       // Clear all menus if the click is outside pod menu and its
1545       // button area.
1546       if (!findAncestorByClass(e.target, 'action-box-menu') &&
1547           !findAncestorByClass(e.target, 'action-box-area')) {
1548         for (var i = 0, pod; pod = this.pods[i]; ++i)
1549           pod.isActionBoxMenuActive = false;
1550       }
1552       // Clears focus if not clicked on a pod and if there's more than one pod.
1553       var pod = findAncestorByClass(e.target, 'pod');
1554       if ((!pod || pod.parentNode != this) && !this.isSinglePod) {
1555         this.focusPod();
1556       }
1558       if (pod)
1559         pod.isActionBoxMenuHovered = true;
1561       // Return focus back to single pod.
1562       if (this.isSinglePod) {
1563         this.focusPod(this.focusedPod_, true /* force */);
1564         if (!pod)
1565           this.focusedPod_.isActionBoxMenuHovered = false;
1566       }
1568       // Also stop event propagation.
1569       if (pod && e.target == pod.imageElement)
1570         e.stopPropagation();
1571     },
1573     /**
1574      * Handler of mouse move event.
1575      * @param {Event} e Click Event object.
1576      * @private
1577      */
1578     handleMouseMove_: function(e) {
1579       if (this.disabled)
1580         return;
1581       if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
1582         return;
1584       // Defocus (thus hide) action box, if it is focused on a user pod
1585       // and the pointer is not hovering over it.
1586       var pod = findAncestorByClass(e.target, 'pod');
1587       if (document.activeElement &&
1588           document.activeElement.parentNode != pod &&
1589           document.activeElement.classList.contains('action-box-area')) {
1590         document.activeElement.parentNode.focus();
1591       }
1593       if (pod)
1594         pod.isActionBoxMenuHovered = true;
1596       // Hide action boxes on other user pods.
1597       for (var i = 0, p; p = this.pods[i]; ++i)
1598         if (p != pod && !p.isActionBoxMenuActive)
1599           p.isActionBoxMenuHovered = false;
1600     },
1602     /**
1603      * Handles focus event.
1604      * @param {Event} e Focus Event object.
1605      * @private
1606      */
1607     handleFocus_: function(e) {
1608       if (this.disabled)
1609         return;
1610       if (e.target.parentNode == this) {
1611         // Focus on a pod
1612         if (e.target.classList.contains('focused'))
1613           e.target.focusInput();
1614         else
1615           this.focusPod(e.target);
1616         return;
1617       }
1619       var pod = findAncestorByClass(e.target, 'pod');
1620       if (pod && pod.parentNode == this) {
1621         // Focus on a control of a pod but not on the action area button.
1622         if (!pod.classList.contains('focused') &&
1623             !e.target.classList.contains('action-box-button')) {
1624           this.focusPod(pod);
1625           e.target.focus();
1626         }
1627         return;
1628       }
1630       // Clears pod focus when we reach here. It means new focus is neither
1631       // on a pod nor on a button/input for a pod.
1632       // Do not "defocus" user pod when it is a single pod.
1633       // That means that 'focused' class will not be removed and
1634       // input field/button will always be visible.
1635       if (!this.isSinglePod)
1636         this.focusPod();
1637     },
1639     /**
1640      * Handler of keydown event.
1641      * @param {Event} e KeyDown Event object.
1642      */
1643     handleKeyDown: function(e) {
1644       if (this.disabled)
1645         return;
1646       var editing = e.target.tagName == 'INPUT' && e.target.value;
1647       switch (e.keyIdentifier) {
1648         case 'Left':
1649           if (!editing) {
1650             this.keyboardActivated_ = true;
1651             if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
1652               this.focusPod(this.focusedPod_.previousElementSibling);
1653             else
1654               this.focusPod(this.lastElementChild);
1656             e.stopPropagation();
1657           }
1658           break;
1659         case 'Right':
1660           if (!editing) {
1661             this.keyboardActivated_ = true;
1662             if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
1663               this.focusPod(this.focusedPod_.nextElementSibling);
1664             else
1665               this.focusPod(this.firstElementChild);
1667             e.stopPropagation();
1668           }
1669           break;
1670         case 'Enter':
1671           if (this.focusedPod_) {
1672             this.activatedPod = this.focusedPod_;
1673             e.stopPropagation();
1674           }
1675           break;
1676         case 'U+001B':  // Esc
1677           if (!this.isSinglePod)
1678             this.focusPod();
1679           break;
1680       }
1681     },
1683     /**
1684      * Called right after the pod row is shown.
1685      */
1686     handleAfterShow: function() {
1687       // Without timeout changes in pods positions will be animated even though
1688       // it happened when 'flying-pods' class was disabled.
1689       setTimeout(function() {
1690         Oobe.getInstance().toggleClass('flying-pods', true);
1691       }, 0);
1692       // Force input focus for user pod on show and once transition ends.
1693       if (this.focusedPod_) {
1694         var focusedPod = this.focusedPod_;
1695         var screen = this.parentNode;
1696         var self = this;
1697         focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
1698           if (e.target == focusedPod) {
1699             focusedPod.removeEventListener('webkitTransitionEnd', f);
1700             focusedPod.reset(true);
1701             // Notify screen that it is ready.
1702             screen.onShow();
1703             self.wallpaperLoader_.scheduleLoad(focusedPod.user.username);
1704           }
1705         });
1706         // Guard timer for 1 second -- it would conver all possible animations.
1707         ensureTransitionEndEvent(focusedPod, 1000);
1708       }
1709     },
1711     /**
1712      * Called right before the pod row is shown.
1713      */
1714     handleBeforeShow: function() {
1715       Oobe.getInstance().toggleClass('flying-pods', false);
1716       for (var event in this.listeners_) {
1717         this.ownerDocument.addEventListener(
1718             event, this.listeners_[event][0], this.listeners_[event][1]);
1719       }
1720       $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
1721     },
1723     /**
1724      * Called when the element is hidden.
1725      */
1726     handleHide: function() {
1727       for (var event in this.listeners_) {
1728         this.ownerDocument.removeEventListener(
1729             event, this.listeners_[event][0], this.listeners_[event][1]);
1730       }
1731       $('login-header-bar').buttonsTabIndex = 0;
1732     },
1734     /**
1735      * Called when a pod's user image finishes loading.
1736      */
1737     handlePodImageLoad: function(pod) {
1738       var index = this.podsWithPendingImages_.indexOf(pod);
1739       if (index == -1) {
1740         return;
1741       }
1743       this.podsWithPendingImages_.splice(index, 1);
1744       if (this.podsWithPendingImages_.length == 0) {
1745         this.classList.remove('images-loading');
1746       }
1747     }
1748   };
1750   return {
1751     PodRow: PodRow
1752   };