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