Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / login / user_pod_row.js
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 /**
6  * @fileoverview User pod row implementation.
7  */
8
9 cr.define('login', function() {
10   /**
11    * Number of displayed columns depending on user pod count.
12    * @type {Array.<number>}
13    * @const
14    */
15   var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6];
16
17   /**
18    * Mapping between number of columns in pod-row and margin between user pods
19    * for such layout.
20    * @type {Array.<number>}
21    * @const
22    */
23   var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12];
24
25   /**
26    * Maximal number of columns currently supported by pod-row.
27    * @type {number}
28    * @const
29    */
30   var MAX_NUMBER_OF_COLUMNS = 6;
31
32   /**
33    * Maximal number of rows if sign-in banner is displayed alonside.
34    * @type {number}
35    * @const
36    */
37   var MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER = 2;
38
39   /**
40    * Variables used for pod placement processing. Width and height should be
41    * synced with computed CSS sizes of pods.
42    */
43   var POD_WIDTH = 180;
44   var PUBLIC_EXPANDED_WIDTH = 420;
45   var CROS_POD_HEIGHT = 213;
46   var DESKTOP_POD_HEIGHT = 216;
47   var POD_ROW_PADDING = 10;
48
49   /**
50    * Whether to preselect the first pod automatically on login screen.
51    * @type {boolean}
52    * @const
53    */
54   var PRESELECT_FIRST_POD = true;
55
56   /**
57    * Maximum time for which the pod row remains hidden until all user images
58    * have been loaded.
59    * @type {number}
60    * @const
61    */
62   var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
63
64   /**
65    * Public session help topic identifier.
66    * @type {number}
67    * @const
68    */
69   var HELP_TOPIC_PUBLIC_SESSION = 3041033;
70
71   /**
72    * Tab order for user pods. Update these when adding new controls.
73    * @enum {number}
74    * @const
75    */
76   var UserPodTabOrder = {
77     POD_INPUT: 1,     // Password input fields (and whole pods themselves).
78     HEADER_BAR: 2,    // Buttons on the header bar (Shutdown, Add User).
79     ACTION_BOX: 3,    // Action box buttons.
80     PAD_MENU_ITEM: 4  // User pad menu items (Remove this user).
81   };
82
83   /**
84    * Supported authentication types. Keep in sync with the enum in
85    * chrome/browser/chromeos/login/login_display.h
86    * @enum {number}
87    * @const
88    */
89   var AUTH_TYPE = {
90     OFFLINE_PASSWORD: 0,
91     ONLINE_SIGN_IN: 1,
92     NUMERIC_PIN: 2,
93     USER_CLICK: 3,
94   };
95
96   /**
97    * Names of authentication types.
98    */
99   var AUTH_TYPE_NAMES = {
100     0: 'offlinePassword',
101     1: 'onlineSignIn',
102     2: 'numericPin',
103     3: 'userClick',
104   };
105
106   // Focus and tab order are organized as follows:
107   //
108   // (1) all user pods have tab index 1 so they are traversed first;
109   // (2) when a user pod is activated, its tab index is set to -1 and its
110   // main input field gets focus and tab index 1;
111   // (3) buttons on the header bar have tab index 2 so they follow user pods;
112   // (4) Action box buttons have tab index 3 and follow header bar buttons;
113   // (5) lastly, focus jumps to the Status Area and back to user pods.
114   //
115   // 'Focus' event is handled by a capture handler for the whole document
116   // and in some cases 'mousedown' event handlers are used instead of 'click'
117   // handlers where it's necessary to prevent 'focus' event from being fired.
118
119   /**
120    * Helper function to remove a class from given element.
121    * @param {!HTMLElement} el Element whose class list to change.
122    * @param {string} cl Class to remove.
123    */
124   function removeClass(el, cl) {
125     el.classList.remove(cl);
126   }
127
128   /**
129    * Creates a user pod.
130    * @constructor
131    * @extends {HTMLDivElement}
132    */
133   var UserPod = cr.ui.define(function() {
134     var node = $('user-pod-template').cloneNode(true);
135     node.removeAttribute('id');
136     return node;
137   });
138
139   /**
140    * Stops event propagation from the any user pod child element.
141    * @param {Event} e Event to handle.
142    */
143   function stopEventPropagation(e) {
144     // Prevent default so that we don't trigger a 'focus' event.
145     e.preventDefault();
146     e.stopPropagation();
147   }
148
149   /**
150    * Unique salt added to user image URLs to prevent caching. Dictionary with
151    * user names as keys.
152    * @type {Object}
153    */
154   UserPod.userImageSalt_ = {};
155
156   UserPod.prototype = {
157     __proto__: HTMLDivElement.prototype,
158
159     /** @override */
160     decorate: function() {
161       this.tabIndex = UserPodTabOrder.POD_INPUT;
162       this.customButtonElement.tabIndex = UserPodTabOrder.POD_INPUT;
163       this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
164
165       this.addEventListener('keydown', this.handlePodKeyDown_.bind(this));
166       this.addEventListener('click', this.handleClickOnPod_.bind(this));
167
168       this.signinButtonElement.addEventListener('click',
169           this.activate.bind(this));
170
171       this.actionBoxAreaElement.addEventListener('mousedown',
172                                                  stopEventPropagation);
173       this.actionBoxAreaElement.addEventListener('click',
174           this.handleActionAreaButtonClick_.bind(this));
175       this.actionBoxAreaElement.addEventListener('keydown',
176           this.handleActionAreaButtonKeyDown_.bind(this));
177
178       this.actionBoxMenuRemoveElement.addEventListener('click',
179           this.handleRemoveCommandClick_.bind(this));
180       this.actionBoxMenuRemoveElement.addEventListener('keydown',
181           this.handleRemoveCommandKeyDown_.bind(this));
182       this.actionBoxMenuRemoveElement.addEventListener('blur',
183           this.handleRemoveCommandBlur_.bind(this));
184
185       if (this.actionBoxRemoveUserWarningButtonElement) {
186         this.actionBoxRemoveUserWarningButtonElement.addEventListener(
187             'click',
188             this.handleRemoveUserConfirmationClick_.bind(this));
189       }
190
191       this.customButtonElement.addEventListener('click',
192           this.handleCustomButtonClick_.bind(this));
193     },
194
195     /**
196      * Initializes the pod after its properties set and added to a pod row.
197      */
198     initialize: function() {
199       this.passwordElement.addEventListener('keydown',
200           this.parentNode.handleKeyDown.bind(this.parentNode));
201       this.passwordElement.addEventListener('keypress',
202           this.handlePasswordKeyPress_.bind(this));
203
204       this.imageElement.addEventListener('load',
205           this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
206
207       var initialAuthType = this.user.initialAuthType ||
208           AUTH_TYPE.OFFLINE_PASSWORD;
209       this.setAuthType(initialAuthType, null);
210     },
211
212     /**
213      * Resets tab order for pod elements to its initial state.
214      */
215     resetTabOrder: function() {
216       // Note: the |mainInput| can be the pod itself.
217       this.mainInput.tabIndex = -1;
218       this.tabIndex = UserPodTabOrder.POD_INPUT;
219     },
220
221     /**
222      * Handles keypress event (i.e. any textual input) on password input.
223      * @param {Event} e Keypress Event object.
224      * @private
225      */
226     handlePasswordKeyPress_: function(e) {
227       // When tabbing from the system tray a tab key press is received. Suppress
228       // this so as not to type a tab character into the password field.
229       if (e.keyCode == 9) {
230         e.preventDefault();
231         return;
232       }
233     },
234
235     /**
236      * Top edge margin number of pixels.
237      * @type {?number}
238      */
239     set top(top) {
240       this.style.top = cr.ui.toCssPx(top);
241     },
242
243     /**
244      * Top edge margin number of pixels.
245      */
246     get top() {
247       return parseInt(this.style.top);
248     },
249
250     /**
251      * Left edge margin number of pixels.
252      * @type {?number}
253      */
254     set left(left) {
255       this.style.left = cr.ui.toCssPx(left);
256     },
257
258     /**
259      * Left edge margin number of pixels.
260      */
261     get left() {
262       return parseInt(this.style.left);
263     },
264
265     /**
266      * Height number of pixels.
267      */
268     get height() {
269       return this.offsetHeight;
270     },
271
272     /**
273      * Gets signed in indicator element.
274      * @type {!HTMLDivElement}
275      */
276     get signedInIndicatorElement() {
277       return this.querySelector('.signed-in-indicator');
278     },
279
280     /**
281      * Gets image element.
282      * @type {!HTMLImageElement}
283      */
284     get imageElement() {
285       return this.querySelector('.user-image');
286     },
287
288     /**
289      * Gets name element.
290      * @type {!HTMLDivElement}
291      */
292     get nameElement() {
293       return this.querySelector('.name');
294     },
295
296     /**
297      * Gets password field.
298      * @type {!HTMLInputElement}
299      */
300     get passwordElement() {
301       return this.querySelector('.password');
302     },
303
304     /**
305      * Gets the password label, which is used to show a message where the
306      * password field is normally.
307      * @type {!HTMLInputElement}
308      */
309     get passwordLabelElement() {
310       return this.querySelector('.password-label');
311     },
312
313     /**
314      * Gets Caps Lock hint image.
315      * @type {!HTMLImageElement}
316      */
317     get capslockHintElement() {
318       return this.querySelector('.capslock-hint');
319     },
320
321     /**
322      * Gets user sign in button.
323      * @type {!HTMLButtonElement}
324      */
325     get signinButtonElement() {
326       return this.querySelector('.signin-button');
327     },
328
329     /**
330      * Gets launch app button.
331      * @type {!HTMLButtonElement}
332      */
333     get launchAppButtonElement() {
334       return this.querySelector('.launch-app-button');
335     },
336
337     /**
338      * Gets action box area.
339      * @type {!HTMLInputElement}
340      */
341     get actionBoxAreaElement() {
342       return this.querySelector('.action-box-area');
343     },
344
345     /**
346      * Gets user type icon area.
347      * @type {!HTMLDivElement}
348      */
349     get userTypeIconAreaElement() {
350       return this.querySelector('.user-type-icon-area');
351     },
352
353     /**
354      * Gets user type icon.
355      * @type {!HTMLDivElement}
356      */
357     get userTypeIconElement() {
358       return this.querySelector('.user-type-icon-image');
359     },
360
361     /**
362      * Gets action box menu.
363      * @type {!HTMLInputElement}
364      */
365     get actionBoxMenuElement() {
366       return this.querySelector('.action-box-menu');
367     },
368
369     /**
370      * Gets action box menu title.
371      * @type {!HTMLInputElement}
372      */
373     get actionBoxMenuTitleElement() {
374       return this.querySelector('.action-box-menu-title');
375     },
376
377     /**
378      * Gets action box menu title, user name item.
379      * @type {!HTMLInputElement}
380      */
381     get actionBoxMenuTitleNameElement() {
382       return this.querySelector('.action-box-menu-title-name');
383     },
384
385     /**
386      * Gets action box menu title, user email item.
387      * @type {!HTMLInputElement}
388      */
389     get actionBoxMenuTitleEmailElement() {
390       return this.querySelector('.action-box-menu-title-email');
391     },
392
393     /**
394      * Gets action box menu, remove user command item.
395      * @type {!HTMLInputElement}
396      */
397     get actionBoxMenuCommandElement() {
398       return this.querySelector('.action-box-menu-remove-command');
399     },
400
401     /**
402      * Gets action box menu, remove user command item div.
403      * @type {!HTMLInputElement}
404      */
405     get actionBoxMenuRemoveElement() {
406       return this.querySelector('.action-box-menu-remove');
407     },
408
409     /**
410      * Gets action box menu, remove user command item div.
411      * @type {!HTMLInputElement}
412      */
413     get actionBoxRemoveUserWarningElement() {
414       return this.querySelector('.action-box-remove-user-warning');
415     },
416
417     /**
418      * Gets action box menu, remove user command item div.
419      * @type {!HTMLInputElement}
420      */
421     get actionBoxRemoveUserWarningButtonElement() {
422       return this.querySelector(
423           '.remove-warning-button');
424     },
425
426     /**
427      * Gets the locked user indicator box.
428      * @type {!HTMLInputElement}
429      */
430     get lockedIndicatorElement() {
431       return this.querySelector('.locked-indicator');
432     },
433
434     /**
435      * Gets the custom button. This button is normally hidden, but can be shown
436      * using the chrome.screenlockPrivate API.
437      * @type {!HTMLInputElement}
438      */
439     get customButtonElement() {
440       return this.querySelector('.custom-button');
441     },
442
443     /**
444      * Updates the user pod element.
445      */
446     update: function() {
447       this.imageElement.src = 'chrome://userimage/' + this.user.username +
448           '?id=' + UserPod.userImageSalt_[this.user.username];
449
450       this.nameElement.textContent = this.user_.displayName;
451       this.signedInIndicatorElement.hidden = !this.user_.signedIn;
452
453       this.signinButtonElement.hidden = !this.isAuthTypeOnlineSignIn;
454       this.customButtonElement.tabIndex = UserPodTabOrder.POD_INPUT;
455       if (this.isAuthTypeUserClick) {
456         this.passwordLabelElement.textContent = this.authValue;
457         this.customButtonElement.tabIndex = -1;
458       }
459
460       this.updateActionBoxArea();
461
462       this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
463         'passwordFieldAccessibleName', this.user_.emailAddress));
464
465       this.customizeUserPodPerUserType();
466     },
467
468     updateActionBoxArea: function() {
469       if (this.user_.publicAccount || this.user_.isApp) {
470         this.actionBoxAreaElement.hidden = true;
471         return;
472       }
473
474       this.actionBoxAreaElement.hidden = false;
475       this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
476
477       this.actionBoxAreaElement.setAttribute(
478           'aria-label', loadTimeData.getStringF(
479               'podMenuButtonAccessibleName', this.user_.emailAddress));
480       this.actionBoxMenuRemoveElement.setAttribute(
481           'aria-label', loadTimeData.getString(
482                'podMenuRemoveItemAccessibleName'));
483       this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
484           loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
485           this.user_.displayName;
486       this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
487       this.actionBoxMenuTitleEmailElement.hidden =
488           this.user_.locallyManagedUser;
489
490       this.actionBoxMenuCommandElement.textContent =
491           loadTimeData.getString('removeUser');
492     },
493
494     customizeUserPodPerUserType: function() {
495       var isMultiProfilesUI =
496           (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING);
497
498       if (this.user_.locallyManagedUser) {
499         this.setUserPodIconType('supervised');
500       } else if (isMultiProfilesUI && !this.user_.isMultiProfilesAllowed) {
501         // Mark user pod as not focusable which in addition to the grayed out
502         // filter makes it look in disabled state.
503         this.classList.add('not-focusable');
504         this.setUserPodIconType('policy');
505
506         this.querySelector('.mp-policy-title').hidden = false;
507         if (this.user.multiProfilesPolicy == 'primary-only')
508           this.querySelector('.mp-policy-primary-only-msg').hidden = false;
509         else
510           this.querySelector('.mp-policy-not-allowed-msg').hidden = false;
511       } else if (this.user_.isApp) {
512         this.setUserPodIconType('app');
513       }
514     },
515
516     setUserPodIconType: function(userTypeClass) {
517       this.userTypeIconAreaElement.classList.add(userTypeClass);
518       this.userTypeIconAreaElement.hidden = false;
519     },
520
521     /**
522      * The user that this pod represents.
523      * @type {!Object}
524      */
525     user_: undefined,
526     get user() {
527       return this.user_;
528     },
529     set user(userDict) {
530       this.user_ = userDict;
531       this.update();
532     },
533
534     /**
535      * Gets main input element.
536      * @type {(HTMLButtonElement|HTMLInputElement)}
537      */
538     get mainInput() {
539       if (this.isAuthTypePassword) {
540         return this.passwordElement;
541       } else if (this.isAuthTypeOnlineSignIn) {
542         return this.signinButtonElement;
543       } else if (this.isAuthTypeUserClick) {
544         return this;
545       }
546     },
547
548     /**
549      * Whether action box button is in active state.
550      * @type {boolean}
551      */
552     get isActionBoxMenuActive() {
553       return this.actionBoxAreaElement.classList.contains('active');
554     },
555     set isActionBoxMenuActive(active) {
556       if (active == this.isActionBoxMenuActive)
557         return;
558
559       if (active) {
560         this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
561         if (this.actionBoxRemoveUserWarningElement)
562           this.actionBoxRemoveUserWarningElement.hidden = true;
563
564         // Clear focus first if another pod is focused.
565         if (!this.parentNode.isFocused(this)) {
566           this.parentNode.focusPod(undefined, true);
567           this.actionBoxAreaElement.focus();
568         }
569         this.actionBoxAreaElement.classList.add('active');
570       } else {
571         this.actionBoxAreaElement.classList.remove('active');
572       }
573     },
574
575     /**
576      * Whether action box button is in hovered state.
577      * @type {boolean}
578      */
579     get isActionBoxMenuHovered() {
580       return this.actionBoxAreaElement.classList.contains('hovered');
581     },
582     set isActionBoxMenuHovered(hovered) {
583       if (hovered == this.isActionBoxMenuHovered)
584         return;
585
586       if (hovered) {
587         this.actionBoxAreaElement.classList.add('hovered');
588         this.classList.add('hovered');
589       } else {
590         this.actionBoxAreaElement.classList.remove('hovered');
591         this.classList.remove('hovered');
592       }
593     },
594
595     /**
596      * Set the authentication type for the pod.
597      * @param {number} An auth type value defined in the AUTH_TYPE enum.
598      * @param {string} authValue The initial value used for the auth type.
599      */
600     setAuthType: function(authType, authValue) {
601       this.authType_ = authType;
602       this.authValue_ = authValue;
603       this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]);
604       this.update();
605       this.reset(this.parentNode.isFocused(this));
606     },
607
608     /**
609      * The auth type of the user pod. This value is one of the enum
610      * values in AUTH_TYPE.
611      * @type {number}
612      */
613     get authType() {
614       return this.authType_;
615     },
616
617     /**
618      * The initial value used for the pod's authentication type.
619      * eg. a prepopulated password input when using password authentication.
620      */
621     get authValue() {
622       return this.authValue_;
623     },
624
625     /**
626      * True if the the user pod uses a password to authenticate.
627      * @type {bool}
628      */
629     get isAuthTypePassword() {
630       return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD;
631     },
632
633     /**
634      * True if the the user pod uses a user click to authenticate.
635      * @type {bool}
636      */
637     get isAuthTypeUserClick() {
638       return this.authType_ == AUTH_TYPE.USER_CLICK;
639     },
640
641     /**
642      * True if the the user pod uses a online sign in to authenticate.
643      * @type {bool}
644      */
645     get isAuthTypeOnlineSignIn() {
646       return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN;
647     },
648
649     /**
650      * Updates the image element of the user.
651      */
652     updateUserImage: function() {
653       UserPod.userImageSalt_[this.user.username] = new Date().getTime();
654       this.update();
655     },
656
657     /**
658      * Focuses on input element.
659      */
660     focusInput: function() {
661       // Move tabIndex from the whole pod to the main input.
662       // Note: the |mainInput| can be the pod itself.
663       this.tabIndex = -1;
664       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
665       this.mainInput.focus();
666     },
667
668     /**
669      * Activates the pod.
670      * @param {Event} e Event object.
671      * @return {boolean} True if activated successfully.
672      */
673     activate: function(e) {
674       if (this.isAuthTypeOnlineSignIn) {
675         this.showSigninUI();
676       } else if (this.isAuthTypeUserClick) {
677         Oobe.disableSigninUI();
678         chrome.send('authenticateUser', [this.user.username, '']);
679       } else if (this.isAuthTypePassword) {
680         if (!this.passwordElement.value)
681           return false;
682         Oobe.disableSigninUI();
683         chrome.send('authenticateUser',
684                     [this.user.username, this.passwordElement.value]);
685       } else {
686         console.error('Activating user pod with invalid authentication type: ' +
687             this.authType);
688       }
689
690       return true;
691     },
692
693     showSupervisedUserSigninWarning: function() {
694       // Locally managed user token has been invalidated.
695       // Make sure that pod is focused i.e. "Sign in" button is seen.
696       this.parentNode.focusPod(this);
697
698       var error = document.createElement('div');
699       var messageDiv = document.createElement('div');
700       messageDiv.className = 'error-message-bubble';
701       messageDiv.textContent =
702           loadTimeData.getString('supervisedUserExpiredTokenWarning');
703       error.appendChild(messageDiv);
704
705       $('bubble').showContentForElement(
706           this.signinButtonElement,
707           cr.ui.Bubble.Attachment.TOP,
708           error,
709           this.signinButtonElement.offsetWidth / 2,
710           4);
711     },
712
713     /**
714      * Shows signin UI for this user.
715      */
716     showSigninUI: function() {
717       if (this.user.locallyManagedUser) {
718         this.showSupervisedUserSigninWarning();
719       } else {
720         this.parentNode.showSigninUI(this.user.emailAddress);
721       }
722     },
723
724     /**
725      * Resets the input field and updates the tab order of pod controls.
726      * @param {boolean} takeFocus If true, input field takes focus.
727      */
728     reset: function(takeFocus) {
729       this.passwordElement.value = '';
730       if (takeFocus)
731         this.focusInput();  // This will set a custom tab order.
732       else
733         this.resetTabOrder();
734     },
735
736     /**
737      * Handles a click event on action area button.
738      * @param {Event} e Click event.
739      */
740     handleActionAreaButtonClick_: function(e) {
741       if (this.parentNode.disabled)
742         return;
743       this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
744       e.stopPropagation();
745     },
746
747     /**
748      * Handles a keydown event on action area button.
749      * @param {Event} e KeyDown event.
750      */
751     handleActionAreaButtonKeyDown_: function(e) {
752       if (this.disabled)
753         return;
754       switch (e.keyIdentifier) {
755         case 'Enter':
756         case 'U+0020':  // Space
757           if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
758             this.isActionBoxMenuActive = true;
759           e.stopPropagation();
760           break;
761         case 'Up':
762         case 'Down':
763           if (this.isActionBoxMenuActive) {
764             this.actionBoxMenuRemoveElement.tabIndex =
765                 UserPodTabOrder.PAD_MENU_ITEM;
766             this.actionBoxMenuRemoveElement.focus();
767           }
768           e.stopPropagation();
769           break;
770         case 'U+001B':  // Esc
771           this.isActionBoxMenuActive = false;
772           e.stopPropagation();
773           break;
774         case 'U+0009':  // Tab
775           this.parentNode.focusPod();
776         default:
777           this.isActionBoxMenuActive = false;
778           break;
779       }
780     },
781
782     /**
783      * Handles a click event on remove user command.
784      * @param {Event} e Click event.
785      */
786     handleRemoveCommandClick_: function(e) {
787       if (this.user.locallyManagedUser || this.user.isDesktopUser) {
788         this.showRemoveWarning_();
789         return;
790       }
791       if (this.isActionBoxMenuActive)
792         chrome.send('removeUser', [this.user.username]);
793     },
794
795     /**
796      * Shows remove warning for managed users.
797      */
798     showRemoveWarning_: function() {
799       this.actionBoxMenuRemoveElement.hidden = true;
800       this.actionBoxRemoveUserWarningElement.hidden = false;
801     },
802
803     /**
804      * Handles a click event on remove user confirmation button.
805      * @param {Event} e Click event.
806      */
807     handleRemoveUserConfirmationClick_: function(e) {
808       if (this.isActionBoxMenuActive)
809         chrome.send('removeUser', [this.user.username]);
810     },
811
812     /**
813      * Handles a keydown event on remove command.
814      * @param {Event} e KeyDown event.
815      */
816     handleRemoveCommandKeyDown_: function(e) {
817       if (this.disabled)
818         return;
819       switch (e.keyIdentifier) {
820         case 'Enter':
821           chrome.send('removeUser', [this.user.username]);
822           e.stopPropagation();
823           break;
824         case 'Up':
825         case 'Down':
826           e.stopPropagation();
827           break;
828         case 'U+001B':  // Esc
829           this.actionBoxAreaElement.focus();
830           this.isActionBoxMenuActive = false;
831           e.stopPropagation();
832           break;
833         default:
834           this.actionBoxAreaElement.focus();
835           this.isActionBoxMenuActive = false;
836           break;
837       }
838     },
839
840     /**
841      * Handles a blur event on remove command.
842      * @param {Event} e Blur event.
843      */
844     handleRemoveCommandBlur_: function(e) {
845       if (this.disabled)
846         return;
847       this.actionBoxMenuRemoveElement.tabIndex = -1;
848     },
849
850     /**
851      * Handles click event on a user pod.
852      * @param {Event} e Click event.
853      */
854     handleClickOnPod_: function(e) {
855       if (this.parentNode.disabled)
856         return;
857
858       if (!this.isActionBoxMenuActive) {
859         if (this.isAuthTypeOnlineSignIn) {
860           this.showSigninUI();
861         } else if (this.isAuthTypeUserClick) {
862           this.parentNode.setActivatedPod(this);
863         }
864
865         // Prevent default so that we don't trigger 'focus' event.
866         e.preventDefault();
867       }
868     },
869
870     /**
871      * Handles keydown event for a user pod.
872      * @param {Event} e Key event.
873      */
874     handlePodKeyDown_: function(e) {
875       if (!this.isAuthTypeUserClick || this.disabled)
876         return;
877       switch (e.keyIdentifier) {
878         case 'Enter':
879         case 'U+0020':  // Space
880           if (this.parentNode.isFocused(this))
881             this.parentNode.setActivatedPod(this);
882           break;
883       }
884     },
885
886     /**
887      * Called when the custom button is clicked.
888      */
889     handleCustomButtonClick_: function() {
890       chrome.send('customButtonClicked', [this.user.username]);
891     }
892   };
893
894   /**
895    * Creates a public account user pod.
896    * @constructor
897    * @extends {UserPod}
898    */
899   var PublicAccountUserPod = cr.ui.define(function() {
900     var node = UserPod();
901
902     var extras = $('public-account-user-pod-extras-template').children;
903     for (var i = 0; i < extras.length; ++i) {
904       var el = extras[i].cloneNode(true);
905       node.appendChild(el);
906     }
907
908     return node;
909   });
910
911   PublicAccountUserPod.prototype = {
912     __proto__: UserPod.prototype,
913
914     /**
915      * "Enter" button in expanded side pane.
916      * @type {!HTMLButtonElement}
917      */
918     get enterButtonElement() {
919       return this.querySelector('.enter-button');
920     },
921
922     /**
923      * Boolean flag of whether the pod is showing the side pane. The flag
924      * controls whether 'expanded' class is added to the pod's class list and
925      * resets tab order because main input element changes when the 'expanded'
926      * state changes.
927      * @type {boolean}
928      */
929     get expanded() {
930       return this.classList.contains('expanded');
931     },
932
933     /**
934      * During transition final height of pod is not available because of
935      * flexbox layout. That's why we have to calculate
936      * the final height manually.
937      */
938     get expandedHeight_() {
939       function getTopAndBottomPadding(domElement) {
940         return parseInt(window.getComputedStyle(
941             domElement).getPropertyValue('padding-top')) +
942             parseInt(window.getComputedStyle(
943                 domElement).getPropertyValue('padding-bottom'));
944       };
945       var height =
946         this.getElementsByClassName('side-pane-contents')[0].offsetHeight +
947         this.getElementsByClassName('enter-button')[0].offsetHeight +
948         getTopAndBottomPadding(
949             this.getElementsByClassName('enter-button')[0]) +
950         getTopAndBottomPadding(
951             this.getElementsByClassName('side-pane-container')[0]) +
952         getTopAndBottomPadding(this);
953       return height;
954     },
955
956     set expanded(expanded) {
957       if (this.expanded == expanded)
958         return;
959
960       this.resetTabOrder();
961       this.classList.toggle('expanded', expanded);
962       if (expanded) {
963         this.usualLeft = this.left;
964         this.usualTop = this.top;
965         if (this.left + PUBLIC_EXPANDED_WIDTH >
966             $('pod-row').offsetWidth - POD_ROW_PADDING)
967           this.left = $('pod-row').offsetWidth - POD_ROW_PADDING -
968               PUBLIC_EXPANDED_WIDTH;
969         var expandedHeight = this.expandedHeight_;
970         if (this.top + expandedHeight > $('pod-row').offsetHeight)
971           this.top = $('pod-row').offsetHeight - expandedHeight;
972       } else {
973         if (typeof(this.usualLeft) != 'undefined')
974           this.left = this.usualLeft;
975         if (typeof(this.usualTop) != 'undefined')
976           this.top = this.usualTop;
977       }
978
979       var self = this;
980       this.classList.add('animating');
981       this.addEventListener('webkitTransitionEnd', function f(e) {
982         self.removeEventListener('webkitTransitionEnd', f);
983         self.classList.remove('animating');
984
985         // Accessibility focus indicator does not move with the focused
986         // element. Sends a 'focus' event on the currently focused element
987         // so that accessibility focus indicator updates its location.
988         if (document.activeElement)
989           document.activeElement.dispatchEvent(new Event('focus'));
990       });
991     },
992
993     /** @override */
994     get mainInput() {
995       if (this.expanded)
996         return this.enterButtonElement;
997       else
998         return this.nameElement;
999     },
1000
1001     /** @override */
1002     decorate: function() {
1003       UserPod.prototype.decorate.call(this);
1004
1005       this.classList.remove('need-password');
1006       this.classList.add('public-account');
1007
1008       this.nameElement.addEventListener('keydown', (function(e) {
1009         if (e.keyIdentifier == 'Enter') {
1010           this.parentNode.setActivatedPod(this, e);
1011           // Stop this keydown event from bubbling up to PodRow handler.
1012           e.stopPropagation();
1013           // Prevent default so that we don't trigger a 'click' event on the
1014           // newly focused "Enter" button.
1015           e.preventDefault();
1016         }
1017       }).bind(this));
1018
1019       var learnMore = this.querySelector('.learn-more');
1020       learnMore.addEventListener('mousedown', stopEventPropagation);
1021       learnMore.addEventListener('click', this.handleLearnMoreEvent);
1022       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1023
1024       learnMore = this.querySelector('.side-pane-learn-more');
1025       learnMore.addEventListener('click', this.handleLearnMoreEvent);
1026       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1027
1028       this.enterButtonElement.addEventListener('click', (function(e) {
1029         this.enterButtonElement.disabled = true;
1030         chrome.send('launchPublicAccount', [this.user.username]);
1031       }).bind(this));
1032     },
1033
1034     /** @override **/
1035     update: function() {
1036       UserPod.prototype.update.call(this);
1037       this.querySelector('.side-pane-name').textContent =
1038           this.user_.displayName;
1039       this.querySelector('.info').textContent =
1040           loadTimeData.getStringF('publicAccountInfoFormat',
1041                                   this.user_.enterpriseDomain);
1042     },
1043
1044     /** @override */
1045     focusInput: function() {
1046       // Move tabIndex from the whole pod to the main input.
1047       this.tabIndex = -1;
1048       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1049       this.mainInput.focus();
1050     },
1051
1052     /** @override */
1053     reset: function(takeFocus) {
1054       if (!takeFocus)
1055         this.expanded = false;
1056       this.enterButtonElement.disabled = false;
1057       UserPod.prototype.reset.call(this, takeFocus);
1058     },
1059
1060     /** @override */
1061     activate: function(e) {
1062       this.expanded = true;
1063       this.focusInput();
1064       return true;
1065     },
1066
1067     /** @override */
1068     handleClickOnPod_: function(e) {
1069       if (this.parentNode.disabled)
1070         return;
1071
1072       this.parentNode.focusPod(this);
1073       this.parentNode.setActivatedPod(this, e);
1074       // Prevent default so that we don't trigger 'focus' event.
1075       e.preventDefault();
1076     },
1077
1078     /**
1079      * Handle mouse and keyboard events for the learn more button. Triggering
1080      * the button causes information about public sessions to be shown.
1081      * @param {Event} event Mouse or keyboard event.
1082      */
1083     handleLearnMoreEvent: function(event) {
1084       switch (event.type) {
1085         // Show informaton on left click. Let any other clicks propagate.
1086         case 'click':
1087           if (event.button != 0)
1088             return;
1089           break;
1090         // Show informaton when <Return> or <Space> is pressed. Let any other
1091         // key presses propagate.
1092         case 'keydown':
1093           switch (event.keyCode) {
1094             case 13:  // Return.
1095             case 32:  // Space.
1096               break;
1097             default:
1098               return;
1099           }
1100           break;
1101       }
1102       chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
1103       stopEventPropagation(event);
1104     },
1105   };
1106
1107   /**
1108    * Creates a user pod to be used only in desktop chrome.
1109    * @constructor
1110    * @extends {UserPod}
1111    */
1112   var DesktopUserPod = cr.ui.define(function() {
1113     // Don't just instantiate a UserPod(), as this will call decorate() on the
1114     // parent object, and add duplicate event listeners.
1115     var node = $('user-pod-template').cloneNode(true);
1116     node.removeAttribute('id');
1117     return node;
1118   });
1119
1120   DesktopUserPod.prototype = {
1121     __proto__: UserPod.prototype,
1122
1123     /** @override */
1124     get mainInput() {
1125       if (!this.passwordElement.hidden)
1126         return this.passwordElement;
1127       else
1128         return this.nameElement;
1129     },
1130
1131     /** @override */
1132     decorate: function() {
1133       UserPod.prototype.decorate.call(this);
1134     },
1135
1136     /** @override */
1137     update: function() {
1138       this.imageElement.src = this.user.userImage;
1139       this.nameElement.textContent = this.user.displayName;
1140
1141       var isLockedUser = this.user.needsSignin;
1142       this.signinButtonElement.hidden = true;
1143       this.lockedIndicatorElement.hidden = !isLockedUser;
1144       this.passwordElement.hidden = !isLockedUser;
1145       this.nameElement.hidden = isLockedUser;
1146
1147       if (this.isAuthTypeUserClick) {
1148         this.passwordLabelElement.textContent = this.authValue;
1149         this.customButtonElement.tabIndex = -1;
1150       }
1151
1152       UserPod.prototype.updateActionBoxArea.call(this);
1153     },
1154
1155     /** @override */
1156     focusInput: function() {
1157       // For focused pods, display the name unless the pod is locked.
1158       var isLockedUser = this.user.needsSignin;
1159       this.signinButtonElement.hidden = true;
1160       this.lockedIndicatorElement.hidden = !isLockedUser;
1161       this.passwordElement.hidden = !isLockedUser;
1162       this.nameElement.hidden = isLockedUser;
1163
1164       // Move tabIndex from the whole pod to the main input.
1165       this.tabIndex = -1;
1166       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1167       this.mainInput.focus();
1168     },
1169
1170     /** @override */
1171     reset: function(takeFocus) {
1172       // Always display the user's name for unfocused pods.
1173       if (!takeFocus)
1174         this.nameElement.hidden = false;
1175       UserPod.prototype.reset.call(this, takeFocus);
1176     },
1177
1178     /** @override */
1179     activate: function(e) {
1180       if (this.passwordElement.hidden) {
1181         Oobe.launchUser(this.user.emailAddress, this.user.displayName);
1182       } else if (!this.passwordElement.value) {
1183         return false;
1184       } else {
1185         chrome.send('authenticatedLaunchUser',
1186                     [this.user.emailAddress,
1187                      this.user.displayName,
1188                      this.passwordElement.value]);
1189       }
1190       this.passwordElement.value = '';
1191       return true;
1192     },
1193
1194     /** @override */
1195     handleClickOnPod_: function(e) {
1196       if (this.parentNode.disabled)
1197         return;
1198
1199       Oobe.clearErrors();
1200       this.parentNode.lastFocusedPod_ = this;
1201
1202       // If this is an unlocked pod, then open a browser window. Otherwise
1203       // just activate the pod and show the password field.
1204       if (!this.user.needsSignin && !this.isActionBoxMenuActive)
1205         this.activate(e);
1206
1207       if (this.isAuthTypeUserClick)
1208         chrome.send('attemptUnlock', [this.user.emailAddress]);
1209     },
1210
1211     /** @override */
1212     handleRemoveUserConfirmationClick_: function(e) {
1213       chrome.send('removeUser', [this.user.profilePath]);
1214     },
1215   };
1216
1217   /**
1218    * Creates a user pod that represents kiosk app.
1219    * @constructor
1220    * @extends {UserPod}
1221    */
1222   var KioskAppPod = cr.ui.define(function() {
1223     var node = UserPod();
1224     return node;
1225   });
1226
1227   KioskAppPod.prototype = {
1228     __proto__: UserPod.prototype,
1229
1230     /** @override */
1231     decorate: function() {
1232       UserPod.prototype.decorate.call(this);
1233       this.launchAppButtonElement.addEventListener('click',
1234                                                    this.activate.bind(this));
1235     },
1236
1237     /** @override */
1238     update: function() {
1239       this.imageElement.src = this.user.iconUrl;
1240       if (this.user.iconHeight && this.user.iconWidth) {
1241         this.imageElement.style.height = this.user.iconHeight;
1242         this.imageElement.style.width = this.user.iconWidth;
1243       }
1244       this.imageElement.alt = this.user.label;
1245       this.imageElement.title = this.user.label;
1246       this.passwordElement.hidden = true;
1247       this.signinButtonElement.hidden = true;
1248       this.launchAppButtonElement.hidden = false;
1249       this.signedInIndicatorElement.hidden = true;
1250       this.nameElement.textContent = this.user.label;
1251
1252       UserPod.prototype.updateActionBoxArea.call(this);
1253       UserPod.prototype.customizeUserPodPerUserType.call(this);
1254     },
1255
1256     /** @override */
1257     get mainInput() {
1258       return this.launchAppButtonElement;
1259     },
1260
1261     /** @override */
1262     focusInput: function() {
1263       this.signinButtonElement.hidden = true;
1264       this.launchAppButtonElement.hidden = false;
1265       this.passwordElement.hidden = true;
1266
1267       // Move tabIndex from the whole pod to the main input.
1268       this.tabIndex = -1;
1269       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1270       this.mainInput.focus();
1271     },
1272
1273     /** @override */
1274     get forceOnlineSignin() {
1275       return false;
1276     },
1277
1278     /** @override */
1279     activate: function(e) {
1280       var diagnosticMode = e && e.ctrlKey;
1281       this.launchApp_(this.user, diagnosticMode);
1282       return true;
1283     },
1284
1285     /** @override */
1286     handleClickOnPod_: function(e) {
1287       if (this.parentNode.disabled)
1288         return;
1289
1290       Oobe.clearErrors();
1291       this.parentNode.lastFocusedPod_ = this;
1292       this.activate(e);
1293     },
1294
1295     /**
1296      * Launch the app. If |diagnosticMode| is true, ask user to confirm.
1297      * @param {Object} app App data.
1298      * @param {boolean} diagnosticMode Whether to run the app in diagnostic
1299      *     mode.
1300      */
1301     launchApp_: function(app, diagnosticMode) {
1302       if (!diagnosticMode) {
1303         chrome.send('launchKioskApp', [app.id, false]);
1304         return;
1305       }
1306
1307       var oobe = $('oobe');
1308       if (!oobe.confirmDiagnosticMode_) {
1309         oobe.confirmDiagnosticMode_ =
1310             new cr.ui.dialogs.ConfirmDialog(document.body);
1311         oobe.confirmDiagnosticMode_.setOkLabel(
1312             loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
1313         oobe.confirmDiagnosticMode_.setCancelLabel(
1314             loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
1315       }
1316
1317       oobe.confirmDiagnosticMode_.show(
1318           loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
1319                                   app.label),
1320           function() {
1321             chrome.send('launchKioskApp', [app.id, true]);
1322           });
1323     },
1324   };
1325
1326   /**
1327    * Creates a new pod row element.
1328    * @constructor
1329    * @extends {HTMLDivElement}
1330    */
1331   var PodRow = cr.ui.define('podrow');
1332
1333   PodRow.prototype = {
1334     __proto__: HTMLDivElement.prototype,
1335
1336     // Whether this user pod row is shown for the first time.
1337     firstShown_: true,
1338
1339     // True if inside focusPod().
1340     insideFocusPod_: false,
1341
1342     // Focused pod.
1343     focusedPod_: undefined,
1344
1345     // Activated pod, i.e. the pod of current login attempt.
1346     activatedPod_: undefined,
1347
1348     // Pod that was most recently focused, if any.
1349     lastFocusedPod_: undefined,
1350
1351     // Pods whose initial images haven't been loaded yet.
1352     podsWithPendingImages_: [],
1353
1354     // Whether pod creation is animated.
1355     userAddIsAnimated_: false,
1356
1357     // Whether pod placement has been postponed.
1358     podPlacementPostponed_: false,
1359
1360     // Standard user pod height/width.
1361     userPodHeight_: 0,
1362     userPodWidth_: 0,
1363
1364     // Array of apps that are shown in addition to other user pods.
1365     apps_: [],
1366
1367     // True to show app pods along with user pods.
1368     shouldShowApps_: true,
1369
1370     // Array of users that are shown (public/supervised/regular).
1371     users_: [],
1372
1373     /** @override */
1374     decorate: function() {
1375       // Event listeners that are installed for the time period during which
1376       // the element is visible.
1377       this.listeners_ = {
1378         focus: [this.handleFocus_.bind(this), true /* useCapture */],
1379         click: [this.handleClick_.bind(this), true],
1380         mousemove: [this.handleMouseMove_.bind(this), false],
1381         keydown: [this.handleKeyDown.bind(this), false]
1382       };
1383
1384       var isDesktopUserManager = Oobe.getInstance().displayType ==
1385           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1386       this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT :
1387                                                    CROS_POD_HEIGHT;
1388       // Same for Chrome OS and desktop.
1389       this.userPodWidth_ = POD_WIDTH;
1390     },
1391
1392     /**
1393      * Returns all the pods in this pod row.
1394      * @type {NodeList}
1395      */
1396     get pods() {
1397       return Array.prototype.slice.call(this.children);
1398     },
1399
1400     /**
1401      * Return true if user pod row has only single user pod in it.
1402      * @type {boolean}
1403      */
1404     get isSinglePod() {
1405       return this.children.length == 1;
1406     },
1407
1408     /**
1409      * Returns pod with the given app id.
1410      * @param {!string} app_id Application id to be matched.
1411      * @return {Object} Pod with the given app id. null if pod hasn't been
1412      *     found.
1413      */
1414     getPodWithAppId_: function(app_id) {
1415       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1416         if (pod.user.isApp && pod.user.id == app_id)
1417           return pod;
1418       }
1419       return null;
1420     },
1421
1422     /**
1423      * Returns pod with the given username (null if there is no such pod).
1424      * @param {string} username Username to be matched.
1425      * @return {Object} Pod with the given username. null if pod hasn't been
1426      *     found.
1427      */
1428     getPodWithUsername_: function(username) {
1429       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1430         if (pod.user.username == username)
1431           return pod;
1432       }
1433       return null;
1434     },
1435
1436     /**
1437      * True if the the pod row is disabled (handles no user interaction).
1438      * @type {boolean}
1439      */
1440     disabled_: false,
1441     get disabled() {
1442       return this.disabled_;
1443     },
1444     set disabled(value) {
1445       this.disabled_ = value;
1446       var controls = this.querySelectorAll('button,input');
1447       for (var i = 0, control; control = controls[i]; ++i) {
1448         control.disabled = value;
1449       }
1450     },
1451
1452     /**
1453      * Creates a user pod from given email.
1454      * @param {!Object} user User info dictionary.
1455      */
1456     createUserPod: function(user) {
1457       var userPod;
1458       if (user.isDesktopUser)
1459         userPod = new DesktopUserPod({user: user});
1460       else if (user.publicAccount)
1461         userPod = new PublicAccountUserPod({user: user});
1462       else if (user.isApp)
1463         userPod = new KioskAppPod({user: user});
1464       else
1465         userPod = new UserPod({user: user});
1466
1467       userPod.hidden = false;
1468       return userPod;
1469     },
1470
1471     /**
1472      * Add an existing user pod to this pod row.
1473      * @param {!Object} user User info dictionary.
1474      * @param {boolean} animated Whether to use init animation.
1475      */
1476     addUserPod: function(user, animated) {
1477       var userPod = this.createUserPod(user);
1478       if (animated) {
1479         userPod.classList.add('init');
1480         userPod.nameElement.classList.add('init');
1481       }
1482
1483       this.appendChild(userPod);
1484       userPod.initialize();
1485     },
1486
1487     /**
1488      * Runs app with a given id from the list of loaded apps.
1489      * @param {!string} app_id of an app to run.
1490      * @param {boolean=} opt_diagnostic_mode Whether to run the app in
1491      *     diagnostic mode. Default is false.
1492      */
1493     findAndRunAppForTesting: function(app_id, opt_diagnostic_mode) {
1494       var app = this.getPodWithAppId_(app_id);
1495       if (app) {
1496         var activationEvent = cr.doc.createEvent('MouseEvents');
1497         var ctrlKey = opt_diagnostic_mode;
1498         activationEvent.initMouseEvent('click', true, true, null,
1499             0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
1500         app.dispatchEvent(activationEvent);
1501       }
1502     },
1503
1504     /**
1505      * Removes user pod from pod row.
1506      * @param {string} email User's email.
1507      */
1508     removeUserPod: function(username) {
1509       var podToRemove = this.getPodWithUsername_(username);
1510       if (podToRemove == null) {
1511         console.warn('Attempt to remove not existing pod for ' + username +
1512             '.');
1513         return;
1514       }
1515       this.removeChild(podToRemove);
1516       if (this.pods.length > 0)
1517         this.placePods_();
1518     },
1519
1520     /**
1521      * Returns index of given pod or -1 if not found.
1522      * @param {UserPod} pod Pod to look up.
1523      * @private
1524      */
1525     indexOf_: function(pod) {
1526       for (var i = 0; i < this.pods.length; ++i) {
1527         if (pod == this.pods[i])
1528           return i;
1529       }
1530       return -1;
1531     },
1532
1533     /**
1534      * Start first time show animation.
1535      */
1536     startInitAnimation: function() {
1537       // Schedule init animation.
1538       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1539         window.setTimeout(removeClass, 500 + i * 70, pod, 'init');
1540         window.setTimeout(removeClass, 700 + i * 70, pod.nameElement, 'init');
1541       }
1542     },
1543
1544     /**
1545      * Start login success animation.
1546      */
1547     startAuthenticatedAnimation: function() {
1548       var activated = this.indexOf_(this.activatedPod_);
1549       if (activated == -1)
1550         return;
1551
1552       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1553         if (i < activated)
1554           pod.classList.add('left');
1555         else if (i > activated)
1556           pod.classList.add('right');
1557         else
1558           pod.classList.add('zoom');
1559       }
1560     },
1561
1562     /**
1563      * Populates pod row with given existing users and start init animation.
1564      * @param {array} users Array of existing user emails.
1565      * @param {boolean} animated Whether to use init animation.
1566      */
1567     loadPods: function(users, animated) {
1568       this.users_ = users;
1569       this.userAddIsAnimated_ = animated;
1570
1571       this.rebuildPods();
1572     },
1573
1574     /**
1575      * Rebuilds pod row using users_ and apps_ that were previously set or
1576      * updated.
1577      */
1578     rebuildPods: function() {
1579       var emptyPodRow = this.pods.length == 0;
1580
1581       // Clear existing pods.
1582       this.innerHTML = '';
1583       this.focusedPod_ = undefined;
1584       this.activatedPod_ = undefined;
1585       this.lastFocusedPod_ = undefined;
1586
1587       // Switch off animation
1588       Oobe.getInstance().toggleClass('flying-pods', false);
1589
1590       // Populate the pod row.
1591       for (var i = 0; i < this.users_.length; ++i)
1592         this.addUserPod(this.users_[i], this.userAddIsAnimated_);
1593
1594       for (var i = 0, pod; pod = this.pods[i]; ++i)
1595         this.podsWithPendingImages_.push(pod);
1596
1597       // TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
1598       if (this.shouldShowApps_) {
1599         for (var i = 0; i < this.apps_.length; ++i)
1600           this.addUserPod(this.apps_[i], this.userAddIsAnimated_);
1601       }
1602
1603       // Make sure we eventually show the pod row, even if some image is stuck.
1604       setTimeout(function() {
1605         $('pod-row').classList.remove('images-loading');
1606       }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
1607
1608       var isCrosAccountPicker = $('login-header-bar').signinUIState ==
1609           SIGNIN_UI_STATE.ACCOUNT_PICKER;
1610       var isDesktopUserManager = Oobe.getInstance().displayType ==
1611           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1612
1613       // Chrome OS: immediately recalculate pods layout only when current UI
1614       //            is account picker. Otherwise postpone it.
1615       // Desktop: recalculate pods layout right away.
1616       if (isDesktopUserManager || isCrosAccountPicker) {
1617         this.placePods_();
1618
1619         // Without timeout changes in pods positions will be animated even
1620         // though it happened when 'flying-pods' class was disabled.
1621         setTimeout(function() {
1622           Oobe.getInstance().toggleClass('flying-pods', true);
1623         }, 0);
1624
1625         this.focusPod(this.preselectedPod);
1626       } else {
1627         this.podPlacementPostponed_ = true;
1628
1629         // Update [Cancel] button state.
1630         if ($('login-header-bar').signinUIState ==
1631                 SIGNIN_UI_STATE.GAIA_SIGNIN &&
1632             emptyPodRow &&
1633             this.pods.length > 0) {
1634           login.GaiaSigninScreen.updateCancelButtonState();
1635         }
1636       }
1637     },
1638
1639     /**
1640      * Adds given apps to the pod row.
1641      * @param {array} apps Array of apps.
1642      */
1643     setApps: function(apps) {
1644       this.apps_ = apps;
1645       this.rebuildPods();
1646       chrome.send('kioskAppsLoaded');
1647
1648       // Check whether there's a pending kiosk app error.
1649       window.setTimeout(function() {
1650         chrome.send('checkKioskAppLaunchError');
1651       }, 500);
1652     },
1653
1654     /**
1655      * Sets whether should show app pods.
1656      * @param {boolean} shouldShowApps Whether app pods should be shown.
1657      */
1658     setShouldShowApps: function(shouldShowApps) {
1659       if (this.shouldShowApps_ == shouldShowApps)
1660         return;
1661
1662       this.shouldShowApps_ = shouldShowApps;
1663       this.rebuildPods();
1664     },
1665
1666     /**
1667      * Shows a button on a user pod with an icon. Clicking on this button
1668      * triggers an event used by the chrome.screenlockPrivate API.
1669      * @param {string} username Username of pod to add button
1670      * @param {string} iconURL URL of the button icon
1671      */
1672     showUserPodButton: function(username, iconURL) {
1673       var pod = this.getPodWithUsername_(username);
1674       if (pod == null) {
1675         console.error('Unable to show user pod button for ' + username +
1676                       ': user pod not found.');
1677         return;
1678       }
1679
1680       pod.customButtonElement.hidden = false;
1681       var icon =
1682           pod.customButtonElement.querySelector('.custom-button-icon');
1683       icon.src = iconURL;
1684     },
1685
1686     /**
1687      * Hides button from user pod added by showUserPodButton().
1688      * @param {string} username Username of pod to remove button
1689      */
1690     hideUserPodButton: function(username) {
1691       var pod = this.getPodWithUsername_(username);
1692       if (pod == null) {
1693         console.error('Unable to hide user pod button for ' + username +
1694                       ': user pod not found.');
1695         return;
1696       }
1697
1698       pod.customButtonElement.hidden = true;
1699     },
1700
1701     /**
1702      * Sets the authentication type used to authenticate the user.
1703      * @param {string} username Username of selected user
1704      * @param {number} authType Authentication type, must be one of the
1705      *                          values listed in AUTH_TYPE enum.
1706      * @param {string} value The initial value to use for authentication.
1707      */
1708     setAuthType: function(username, authType, value) {
1709       var pod = this.getPodWithUsername_(username);
1710       if (pod == null) {
1711         console.error('Unable to set auth type for ' + username +
1712                       ': user pod not found.');
1713         return;
1714       }
1715       pod.setAuthType(authType, value);
1716     },
1717
1718     /**
1719      * Shows a tooltip bubble explaining Easy Unlock for the focused pod.
1720      */
1721     showEasyUnlockBubble: function() {
1722       if (!this.focusedPod_) {
1723         console.error('No focused pod to show Easy Unlock bubble.');
1724         return;
1725       }
1726
1727       var bubbleContent = document.createElement('div');
1728       bubbleContent.classList.add('easy-unlock-button-content');
1729       bubbleContent.textContent = loadTimeData.getString('easyUnlockTooltip');
1730
1731       var attachElement = this.focusedPod_.customButtonElement;
1732       /** @const */ var BUBBLE_OFFSET = 20;
1733       /** @const */ var BUBBLE_PADDING = 8;
1734       $('bubble').showContentForElement(attachElement,
1735                                         cr.ui.Bubble.Attachment.RIGHT,
1736                                         bubbleContent,
1737                                         BUBBLE_OFFSET,
1738                                         BUBBLE_PADDING);
1739     },
1740
1741     /**
1742      * Called when window was resized.
1743      */
1744     onWindowResize: function() {
1745       var layout = this.calculateLayout_();
1746       if (layout.columns != this.columns || layout.rows != this.rows)
1747         this.placePods_();
1748     },
1749
1750     /**
1751      * Returns width of podrow having |columns| number of columns.
1752      * @private
1753      */
1754     columnsToWidth_: function(columns) {
1755       var margin = MARGIN_BY_COLUMNS[columns];
1756       return 2 * POD_ROW_PADDING + columns *
1757           this.userPodWidth_ + (columns - 1) * margin;
1758     },
1759
1760     /**
1761      * Returns height of podrow having |rows| number of rows.
1762      * @private
1763      */
1764     rowsToHeight_: function(rows) {
1765       return 2 * POD_ROW_PADDING + rows * this.userPodHeight_;
1766     },
1767
1768     /**
1769      * Calculates number of columns and rows that podrow should have in order to
1770      * hold as much its pods as possible for current screen size. Also it tries
1771      * to choose layout that looks good.
1772      * @return {{columns: number, rows: number}}
1773      */
1774     calculateLayout_: function() {
1775       var preferredColumns = this.pods.length < COLUMNS.length ?
1776           COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
1777       var maxWidth = Oobe.getInstance().clientAreaSize.width;
1778       var columns = preferredColumns;
1779       while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
1780         --columns;
1781       var rows = Math.floor((this.pods.length - 1) / columns) + 1;
1782       if (getComputedStyle(
1783           $('signin-banner'), null).getPropertyValue('display') != 'none') {
1784         rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER);
1785       }
1786       var maxHeigth = Oobe.getInstance().clientAreaSize.height;
1787       while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
1788         --rows;
1789       // One more iteration if it's not enough cells to place all pods.
1790       while (maxWidth >= this.columnsToWidth_(columns + 1) &&
1791              columns * rows < this.pods.length &&
1792              columns < MAX_NUMBER_OF_COLUMNS) {
1793          ++columns;
1794       }
1795       return {columns: columns, rows: rows};
1796     },
1797
1798     /**
1799      * Places pods onto their positions onto pod grid.
1800      * @private
1801      */
1802     placePods_: function() {
1803       var layout = this.calculateLayout_();
1804       var columns = this.columns = layout.columns;
1805       var rows = this.rows = layout.rows;
1806       var maxPodsNumber = columns * rows;
1807       var margin = MARGIN_BY_COLUMNS[columns];
1808       this.parentNode.setPreferredSize(
1809           this.columnsToWidth_(columns), this.rowsToHeight_(rows));
1810       var height = this.userPodHeight_;
1811       var width = this.userPodWidth_;
1812       this.pods.forEach(function(pod, index) {
1813         if (pod.offsetHeight != height) {
1814           console.error('Pod offsetHeight (' + pod.offsetHeight +
1815               ') and POD_HEIGHT (' + height + ') are not equal.');
1816         }
1817         if (pod.offsetWidth != width) {
1818           console.error('Pod offsetWidth (' + pod.offsetWidth +
1819               ') and POD_WIDTH (' + width + ') are not equal.');
1820         }
1821         if (index >= maxPodsNumber) {
1822            pod.hidden = true;
1823            return;
1824         }
1825         pod.hidden = false;
1826         var column = index % columns;
1827         var row = Math.floor(index / columns);
1828         pod.left = POD_ROW_PADDING + column * (width + margin);
1829         pod.top = POD_ROW_PADDING + row * height;
1830       });
1831       Oobe.getInstance().updateScreenSize(this.parentNode);
1832     },
1833
1834     /**
1835      * Number of columns.
1836      * @type {?number}
1837      */
1838     set columns(columns) {
1839       // Cannot use 'columns' here.
1840       this.setAttribute('ncolumns', columns);
1841     },
1842     get columns() {
1843       return this.getAttribute('ncolumns');
1844     },
1845
1846     /**
1847      * Number of rows.
1848      * @type {?number}
1849      */
1850     set rows(rows) {
1851       // Cannot use 'rows' here.
1852       this.setAttribute('nrows', rows);
1853     },
1854     get rows() {
1855       return this.getAttribute('nrows');
1856     },
1857
1858     /**
1859      * Whether the pod is currently focused.
1860      * @param {UserPod} pod Pod to check for focus.
1861      * @return {boolean} Pod focus status.
1862      */
1863     isFocused: function(pod) {
1864       return this.focusedPod_ == pod;
1865     },
1866
1867     /**
1868      * Focuses a given user pod or clear focus when given null.
1869      * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
1870      * @param {boolean=} opt_force If true, forces focus update even when
1871      *     podToFocus is already focused.
1872      */
1873     focusPod: function(podToFocus, opt_force) {
1874       if (this.isFocused(podToFocus) && !opt_force) {
1875         this.keyboardActivated_ = false;
1876         return;
1877       }
1878
1879       // Make sure that we don't focus pods that are not allowed to be focused.
1880       // TODO(nkostylev): Fix various keyboard focus related issues caused
1881       // by this approach. http://crbug.com/339042
1882       if (podToFocus && podToFocus.classList.contains('not-focusable')) {
1883         this.keyboardActivated_ = false;
1884         return;
1885       }
1886
1887       // Make sure there's only one focusPod operation happening at a time.
1888       if (this.insideFocusPod_) {
1889         this.keyboardActivated_ = false;
1890         return;
1891       }
1892       this.insideFocusPod_ = true;
1893
1894       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1895         if (!this.isSinglePod) {
1896           pod.isActionBoxMenuActive = false;
1897         }
1898         if (pod != podToFocus) {
1899           pod.isActionBoxMenuHovered = false;
1900           pod.classList.remove('focused');
1901           pod.classList.remove('faded');
1902           pod.reset(false);
1903         }
1904       }
1905
1906       // Clear any error messages for previous pod.
1907       if (!this.isFocused(podToFocus))
1908         Oobe.clearErrors();
1909
1910       var hadFocus = !!this.focusedPod_;
1911       this.focusedPod_ = podToFocus;
1912       if (podToFocus) {
1913         podToFocus.classList.remove('faded');
1914         podToFocus.classList.add('focused');
1915         podToFocus.reset(true);  // Reset and give focus.
1916         // focusPod() automatically loads wallpaper
1917         if (!podToFocus.user.isApp)
1918           chrome.send('focusPod', [podToFocus.user.username]);
1919         this.firstShown_ = false;
1920         this.lastFocusedPod_ = podToFocus;
1921       }
1922       this.insideFocusPod_ = false;
1923       this.keyboardActivated_ = false;
1924     },
1925
1926     /**
1927      * Focuses a given user pod by index or clear focus when given null.
1928      * @param {int=} podToFocus index of User pod to focus.
1929      * @param {boolean=} opt_force If true, forces focus update even when
1930      *     podToFocus is already focused.
1931      */
1932     focusPodByIndex: function(podToFocus, opt_force) {
1933       if (podToFocus < this.pods.length)
1934         this.focusPod(this.pods[podToFocus], opt_force);
1935     },
1936
1937     /**
1938      * Resets wallpaper to the last active user's wallpaper, if any.
1939      */
1940     loadLastWallpaper: function() {
1941       if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp)
1942         chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
1943     },
1944
1945     /**
1946      * Returns the currently activated pod.
1947      * @type {UserPod}
1948      */
1949     get activatedPod() {
1950       return this.activatedPod_;
1951     },
1952
1953     /**
1954      * Sets currently activated pod.
1955      * @param {UserPod} pod Pod to check for focus.
1956      * @param {Event} e Event object.
1957      */
1958     setActivatedPod: function(pod, e) {
1959       if (pod && pod.activate(e))
1960         this.activatedPod_ = pod;
1961     },
1962
1963     /**
1964      * The pod of the signed-in user, if any; null otherwise.
1965      * @type {?UserPod}
1966      */
1967     get lockedPod() {
1968       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1969         if (pod.user.signedIn)
1970           return pod;
1971       }
1972       return null;
1973     },
1974
1975     /**
1976      * The pod that is preselected on user pod row show.
1977      * @type {?UserPod}
1978      */
1979     get preselectedPod() {
1980       var lockedPod = this.lockedPod;
1981       var preselectedPod = PRESELECT_FIRST_POD ?
1982           lockedPod || this.pods[0] : lockedPod;
1983       return preselectedPod;
1984     },
1985
1986     /**
1987      * Resets input UI.
1988      * @param {boolean} takeFocus True to take focus.
1989      */
1990     reset: function(takeFocus) {
1991       this.disabled = false;
1992       if (this.activatedPod_)
1993         this.activatedPod_.reset(takeFocus);
1994     },
1995
1996     /**
1997      * Restores input focus to current selected pod, if there is any.
1998      */
1999     refocusCurrentPod: function() {
2000       if (this.focusedPod_) {
2001         this.focusedPod_.focusInput();
2002       }
2003     },
2004
2005     /**
2006      * Clears focused pod password field.
2007      */
2008     clearFocusedPod: function() {
2009       if (!this.disabled && this.focusedPod_)
2010         this.focusedPod_.reset(true);
2011     },
2012
2013     /**
2014      * Shows signin UI.
2015      * @param {string} email Email for signin UI.
2016      */
2017     showSigninUI: function(email) {
2018       // Clear any error messages that might still be around.
2019       Oobe.clearErrors();
2020       this.disabled = true;
2021       this.lastFocusedPod_ = this.getPodWithUsername_(email);
2022       Oobe.showSigninUI(email);
2023     },
2024
2025     /**
2026      * Updates current image of a user.
2027      * @param {string} username User for which to update the image.
2028      */
2029     updateUserImage: function(username) {
2030       var pod = this.getPodWithUsername_(username);
2031       if (pod)
2032         pod.updateUserImage();
2033     },
2034
2035     /**
2036      * Handler of click event.
2037      * @param {Event} e Click Event object.
2038      * @private
2039      */
2040     handleClick_: function(e) {
2041       if (this.disabled)
2042         return;
2043
2044       // Clear all menus if the click is outside pod menu and its
2045       // button area.
2046       if (!findAncestorByClass(e.target, 'action-box-menu') &&
2047           !findAncestorByClass(e.target, 'action-box-area')) {
2048         for (var i = 0, pod; pod = this.pods[i]; ++i)
2049           pod.isActionBoxMenuActive = false;
2050       }
2051
2052       // Clears focus if not clicked on a pod and if there's more than one pod.
2053       var pod = findAncestorByClass(e.target, 'pod');
2054       if ((!pod || pod.parentNode != this) && !this.isSinglePod) {
2055         this.focusPod();
2056       }
2057
2058       if (pod)
2059         pod.isActionBoxMenuHovered = true;
2060
2061       // Return focus back to single pod.
2062       if (this.isSinglePod) {
2063         this.focusPod(this.focusedPod_, true /* force */);
2064         if (!pod)
2065           this.focusedPod_.isActionBoxMenuHovered = false;
2066       }
2067     },
2068
2069     /**
2070      * Handler of mouse move event.
2071      * @param {Event} e Click Event object.
2072      * @private
2073      */
2074     handleMouseMove_: function(e) {
2075       if (this.disabled)
2076         return;
2077       if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
2078         return;
2079
2080       // Defocus (thus hide) action box, if it is focused on a user pod
2081       // and the pointer is not hovering over it.
2082       var pod = findAncestorByClass(e.target, 'pod');
2083       if (document.activeElement &&
2084           document.activeElement.parentNode != pod &&
2085           document.activeElement.classList.contains('action-box-area')) {
2086         document.activeElement.parentNode.focus();
2087       }
2088
2089       if (pod)
2090         pod.isActionBoxMenuHovered = true;
2091
2092       // Hide action boxes on other user pods.
2093       for (var i = 0, p; p = this.pods[i]; ++i)
2094         if (p != pod && !p.isActionBoxMenuActive)
2095           p.isActionBoxMenuHovered = false;
2096     },
2097
2098     /**
2099      * Handles focus event.
2100      * @param {Event} e Focus Event object.
2101      * @private
2102      */
2103     handleFocus_: function(e) {
2104       if (this.disabled)
2105         return;
2106       if (e.target.parentNode == this) {
2107         // Focus on a pod
2108         if (e.target.classList.contains('focused'))
2109           e.target.focusInput();
2110         else
2111           this.focusPod(e.target);
2112         return;
2113       }
2114
2115       var pod = findAncestorByClass(e.target, 'pod');
2116       if (pod && pod.parentNode == this) {
2117         // Focus on a control of a pod but not on the action area button.
2118         if (!pod.classList.contains('focused') &&
2119             !e.target.classList.contains('action-box-button')) {
2120           this.focusPod(pod);
2121           e.target.focus();
2122         }
2123         return;
2124       }
2125
2126       // Clears pod focus when we reach here. It means new focus is neither
2127       // on a pod nor on a button/input for a pod.
2128       // Do not "defocus" user pod when it is a single pod.
2129       // That means that 'focused' class will not be removed and
2130       // input field/button will always be visible.
2131       if (!this.isSinglePod)
2132         this.focusPod();
2133     },
2134
2135     /**
2136      * Handler of keydown event.
2137      * @param {Event} e KeyDown Event object.
2138      */
2139     handleKeyDown: function(e) {
2140       if (this.disabled)
2141         return;
2142       var editing = e.target.tagName == 'INPUT' && e.target.value;
2143       switch (e.keyIdentifier) {
2144         case 'Left':
2145           if (!editing) {
2146             this.keyboardActivated_ = true;
2147             if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
2148               this.focusPod(this.focusedPod_.previousElementSibling);
2149             else
2150               this.focusPod(this.lastElementChild);
2151
2152             e.stopPropagation();
2153           }
2154           break;
2155         case 'Right':
2156           if (!editing) {
2157             this.keyboardActivated_ = true;
2158             if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
2159               this.focusPod(this.focusedPod_.nextElementSibling);
2160             else
2161               this.focusPod(this.firstElementChild);
2162
2163             e.stopPropagation();
2164           }
2165           break;
2166         case 'Enter':
2167           if (this.focusedPod_) {
2168             var targetTag = e.target.tagName;
2169             if (e.target == this.focusedPod_.passwordElement ||
2170                 (targetTag != 'INPUT' &&
2171                  targetTag != 'BUTTON' &&
2172                  targetTag != 'A')) {
2173               this.setActivatedPod(this.focusedPod_, e);
2174               e.stopPropagation();
2175             }
2176           }
2177           break;
2178         case 'U+001B':  // Esc
2179           if (!this.isSinglePod)
2180             this.focusPod();
2181           break;
2182       }
2183     },
2184
2185     /**
2186      * Called right after the pod row is shown.
2187      */
2188     handleAfterShow: function() {
2189       // Without timeout changes in pods positions will be animated even though
2190       // it happened when 'flying-pods' class was disabled.
2191       setTimeout(function() {
2192         Oobe.getInstance().toggleClass('flying-pods', true);
2193       }, 0);
2194       // Force input focus for user pod on show and once transition ends.
2195       if (this.focusedPod_) {
2196         var focusedPod = this.focusedPod_;
2197         var screen = this.parentNode;
2198         var self = this;
2199         focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
2200           focusedPod.removeEventListener('webkitTransitionEnd', f);
2201           focusedPod.reset(true);
2202           // Notify screen that it is ready.
2203           screen.onShow();
2204         });
2205         // Guard timer for 1 second -- it would conver all possible animations.
2206         ensureTransitionEndEvent(focusedPod, 1000);
2207       }
2208     },
2209
2210     /**
2211      * Called right before the pod row is shown.
2212      */
2213     handleBeforeShow: function() {
2214       Oobe.getInstance().toggleClass('flying-pods', false);
2215       for (var event in this.listeners_) {
2216         this.ownerDocument.addEventListener(
2217             event, this.listeners_[event][0], this.listeners_[event][1]);
2218       }
2219       $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
2220
2221       if (this.podPlacementPostponed_) {
2222         this.podPlacementPostponed_ = false;
2223         this.placePods_();
2224         this.focusPod(this.preselectedPod);
2225       }
2226     },
2227
2228     /**
2229      * Called when the element is hidden.
2230      */
2231     handleHide: function() {
2232       for (var event in this.listeners_) {
2233         this.ownerDocument.removeEventListener(
2234             event, this.listeners_[event][0], this.listeners_[event][1]);
2235       }
2236       $('login-header-bar').buttonsTabIndex = 0;
2237     },
2238
2239     /**
2240      * Called when a pod's user image finishes loading.
2241      */
2242     handlePodImageLoad: function(pod) {
2243       var index = this.podsWithPendingImages_.indexOf(pod);
2244       if (index == -1) {
2245         return;
2246       }
2247
2248       this.podsWithPendingImages_.splice(index, 1);
2249       if (this.podsWithPendingImages_.length == 0) {
2250         this.classList.remove('images-loading');
2251       }
2252     }
2253   };
2254
2255   return {
2256     PodRow: PodRow
2257   };
2258 });