Upstream version 10.38.220.0
[platform/framework/web/crosswalk.git] / src / ui / login / account_picker / 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    * Mapping between number of columns in the desktop pod-row and margin
27    * between user pods for such layout.
28    * @type {Array.<number>}
29    * @const
30    */
31   var DESKTOP_MARGIN_BY_COLUMNS = [undefined, 15, 15, 15, 15, 15, 15];
32
33   /**
34    * Maximal number of columns currently supported by pod-row.
35    * @type {number}
36    * @const
37    */
38   var MAX_NUMBER_OF_COLUMNS = 6;
39
40   /**
41    * Maximal number of rows if sign-in banner is displayed alonside.
42    * @type {number}
43    * @const
44    */
45   var MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER = 2;
46
47   /**
48    * Variables used for pod placement processing. Width and height should be
49    * synced with computed CSS sizes of pods.
50    */
51   var POD_WIDTH = 180;
52   var PUBLIC_EXPANDED_BASIC_WIDTH = 500;
53   var PUBLIC_EXPANDED_ADVANCED_WIDTH = 610;
54   var CROS_POD_HEIGHT = 213;
55   var DESKTOP_POD_HEIGHT = 226;
56   var POD_ROW_PADDING = 10;
57   var DESKTOP_ROW_PADDING = 15;
58   var CUSTOM_ICON_CONTAINER_SIZE = 40;
59
60   /**
61    * Minimal padding between user pod and virtual keyboard.
62    * @type {number}
63    * @const
64    */
65   var USER_POD_KEYBOARD_MIN_PADDING = 20;
66
67   /**
68    * Maximum time for which the pod row remains hidden until all user images
69    * have been loaded.
70    * @type {number}
71    * @const
72    */
73   var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
74
75   /**
76    * Public session help topic identifier.
77    * @type {number}
78    * @const
79    */
80   var HELP_TOPIC_PUBLIC_SESSION = 3041033;
81
82   /**
83    * Tab order for user pods. Update these when adding new controls.
84    * @enum {number}
85    * @const
86    */
87   var UserPodTabOrder = {
88     POD_INPUT: 1,        // Password input fields (and whole pods themselves).
89     POD_CUSTOM_ICON: 2,  // Pod custom icon next to passwrod input field.
90     HEADER_BAR: 3,       // Buttons on the header bar (Shutdown, Add User).
91     ACTION_BOX: 4,       // Action box buttons.
92     PAD_MENU_ITEM: 5     // User pad menu items (Remove this user).
93   };
94
95   /**
96    * Supported authentication types. Keep in sync with the enum in
97    * chrome/browser/signin/screenlock_bridge.h
98    * @enum {number}
99    * @const
100    */
101   var AUTH_TYPE = {
102     OFFLINE_PASSWORD: 0,
103     ONLINE_SIGN_IN: 1,
104     NUMERIC_PIN: 2,
105     USER_CLICK: 3,
106     EXPAND_THEN_USER_CLICK: 4,
107     FORCE_OFFLINE_PASSWORD: 5
108   };
109
110   /**
111    * Names of authentication types.
112    */
113   var AUTH_TYPE_NAMES = {
114     0: 'offlinePassword',
115     1: 'onlineSignIn',
116     2: 'numericPin',
117     3: 'userClick',
118     4: 'expandThenUserClick',
119     5: 'forceOfflinePassword'
120   };
121
122   // Focus and tab order are organized as follows:
123   //
124   // (1) all user pods have tab index 1 so they are traversed first;
125   // (2) when a user pod is activated, its tab index is set to -1 and its
126   // main input field gets focus and tab index 1;
127   // (3) if user pod custom icon is interactive, it has tab index 2 so it
128   // follows the input.
129   // (4) buttons on the header bar have tab index 3 so they follow the custom
130   // icon, or user pod if custom icon is not interactive;
131   // (5) Action box buttons have tab index 4 and follow header bar buttons;
132   // (6) lastly, focus jumps to the Status Area and back to user pods.
133   //
134   // 'Focus' event is handled by a capture handler for the whole document
135   // and in some cases 'mousedown' event handlers are used instead of 'click'
136   // handlers where it's necessary to prevent 'focus' event from being fired.
137
138   /**
139    * Helper function to remove a class from given element.
140    * @param {!HTMLElement} el Element whose class list to change.
141    * @param {string} cl Class to remove.
142    */
143   function removeClass(el, cl) {
144     el.classList.remove(cl);
145   }
146
147   /**
148    * Creates a user pod.
149    * @constructor
150    * @extends {HTMLDivElement}
151    */
152   var UserPod = cr.ui.define(function() {
153     var node = $('user-pod-template').cloneNode(true);
154     node.removeAttribute('id');
155     return node;
156   });
157
158   /**
159    * Stops event propagation from the any user pod child element.
160    * @param {Event} e Event to handle.
161    */
162   function stopEventPropagation(e) {
163     // Prevent default so that we don't trigger a 'focus' event.
164     e.preventDefault();
165     e.stopPropagation();
166   }
167
168   /**
169    * Creates an element for custom icon shown in a user pod next to the input
170    * field.
171    * @constructor
172    * @extends {HTMLDivElement}
173    */
174   var UserPodCustomIcon = cr.ui.define(function() {
175     var node = document.createElement('div');
176     node.classList.add('custom-icon-container');
177     node.hidden = true;
178
179     // Create the actual icon element and add it as a child to the container.
180     var iconNode = document.createElement('div');
181     iconNode.classList.add('custom-icon');
182     node.appendChild(iconNode);
183     return node;
184   });
185
186   UserPodCustomIcon.prototype = {
187     __proto__: HTMLDivElement.prototype,
188
189     /**
190      * The icon height.
191      * @type {number}
192      * @private
193      */
194     height_: 0,
195
196     /**
197      * The icon width.
198      * @type {number}
199      * @private
200      */
201     width_: 0,
202
203     /**
204      * Tooltip to be shown when the user hovers over the icon. The icon
205      * properties may be set so the tooltip is shown automatically when the icon
206      * is updated. The tooltip is shown in a bubble attached to the icon
207      * element.
208      * @type {string}
209      * @private
210      */
211     tooltip_: '',
212
213     /**
214      * Whether the tooltip is shown and the user is hovering over the icon.
215      * @type {boolean}
216      * @private
217      */
218     tooltipActive_: false,
219
220     /**
221      * Whether the icon has been shown as a result of |autoshow| parameter begin
222      * set rather than user hovering over the icon.
223      * If this is set, the tooltip will not be removed when the mouse leaves the
224      * icon.
225      * @type {boolean}
226      * @private
227      */
228     tooltipAutoshown_: false,
229
230     /**
231      * A reference to the timeout for showing tooltip after mouse enters the
232      * icon.
233      * @type {?number}
234      * @private
235      */
236     showTooltipTimeout_: null,
237
238     /**
239      * If animation is set, the current horizontal background offset for the
240      * icon resource.
241      * @type {number}
242      * @private
243      */
244     lastAnimationOffset_: 0,
245
246     /**
247      * The reference to interval for progressing the animation.
248      * @type {?number}
249      * @private
250      */
251     animationInterval_: null,
252
253     /**
254      * The width of the resource that contains representations for different
255      * animation stages.
256      * @type {number}
257      * @private
258      */
259     animationResourceSize_: 0,
260
261     /**
262      * When {@code fadeOut} is called, the element gets hidden using fadeout
263      * animation. This is reference to the listener for transition end added to
264      * the icon element while it's fading out.
265      * @type {?function(Event)}
266      * @private
267      */
268     hideTransitionListener_: null,
269
270     /**
271      * Callback for click and 'Enter' key events that gets set if the icon is
272      * interactive.
273      * @type {?function()}
274      * @private
275      */
276     actionHandler_: null,
277
278     /** @override */
279     decorate: function() {
280       this.iconElement.addEventListener('mouseover',
281                                         this.showTooltipSoon_.bind(this));
282       this.iconElement.addEventListener('mouseout',
283                                          this.hideTooltip_.bind(this, false));
284       this.iconElement.addEventListener('mousedown',
285                                          this.hideTooltip_.bind(this, false));
286       this.iconElement.addEventListener('click',
287                                         this.handleClick_.bind(this));
288       this.iconElement.addEventListener('keydown',
289                                         this.handleKeyDown_.bind(this));
290
291       // When the icon is focused using mouse, there should be no outline shown.
292       // Preventing default mousedown event accomplishes this.
293       this.iconElement.addEventListener('mousedown', function(e) {
294         e.preventDefault();
295       });
296     },
297
298     /**
299      * Getter for the icon element's div.
300      * @return {HTMLDivElement}
301      */
302     get iconElement() {
303       return this.querySelector('.custom-icon');
304     },
305
306     /**
307      * Set the icon's background image as image set with different
308      * representations for available screen scale factors.
309      * @param {!{scale1x: string, scale2x: string}} icon The icon
310      *     representations.
311      */
312     setIconAsImageSet: function(icon) {
313       this.iconElement.style.backgroundImage =
314           '-webkit-image-set(' +
315               'url(' + icon.scale1x + ') 1x,' +
316               'url(' + icon.scale2x + ') 2x)';
317     },
318
319     /**
320      * Sets the icon background image to a chrome://theme URL.
321      * @param {!string} iconUrl The icon's background image URL.
322      */
323     setIconAsResourceUrl: function(iconUrl) {
324       this.iconElement.style.backgroundImage =
325           '-webkit-image-set(' +
326               'url(' + iconUrl + '@1x) 1x,' +
327               'url(' + iconUrl + '@2x) 2x)';
328     },
329
330     /**
331      * Shows the icon.
332      */
333     show: function() {
334       this.resetHideTransitionState_();
335       this.hidden = false;
336     },
337
338     /**
339      * Hides the icon using a fade-out animation.
340      */
341     fadeOut: function() {
342       // The icon is already being hidden.
343       if (this.iconElement.classList.contains('faded'))
344         return;
345
346       this.hideTooltip_(true);
347       this.iconElement.classList.add('faded');
348       this.hideTransitionListener_ = this.hide.bind(this);
349       this.iconElement.addEventListener('webkitTransitionEnd',
350                                         this.hideTransitionListener_);
351       ensureTransitionEndEvent(this.iconElement, 200);
352     },
353
354     /**
355      * Sets the icon size element size.
356      * @param {!{width: number, height: number}} size The icon size.
357      */
358     setSize: function(size) {
359       this.height_ = size.height < CUSTOM_ICON_CONTAINER_SIZE ?
360           size.height : CUSTOM_ICON_COTAINER_SIZE;
361       this.iconElement.style.height = this.height_ + 'px';
362
363       this.width_ = size.width < CUSTOM_ICON_CONTAINER_SIZE ?
364           size.width : CUSTOM_ICON_COTAINER_SIZE;
365       this.iconElement.style.width = this.width_ + 'px';
366       this.style.width = this.width_ + 'px';
367     },
368
369     /**
370      * Sets the icon opacity.
371      * @param {number} opacity The icon opacity in [0-100] scale.
372      */
373     setOpacity: function(opacity) {
374       if (opacity > 100) {
375         this.style.opacity = 1;
376       } else if (opacity < 0) {
377         this.style.opacity = 0;
378       } else {
379         this.style.opacity = opacity / 100;
380       }
381     },
382
383     /**
384      * Updates the icon tooltip. If {@code autoshow} parameter is set the
385      * tooltip is immediatelly shown. If tooltip text is not set, the method
386      * ensures the tooltip gets hidden. If tooltip is shown prior to this call,
387      * it remains shown, but the tooltip text is updated.
388      * @param {!{text: string, autoshow: boolean}} tooltip The tooltip
389      *    parameters.
390      */
391     setTooltip: function(tooltip) {
392       if (this.tooltip_ == tooltip.text && !tooltip.autoshow)
393         return;
394       this.tooltip_ = tooltip.text;
395
396       // If tooltip is already shown, just update the text.
397       if (tooltip.text && this.tooltipActive_ && !$('bubble').hidden) {
398         // If both previous and the new tooltip are supposed to be shown
399         // automatically, keep the autoshown flag.
400         var markAutoshown = this.tooltipAutoshown_ && tooltip.autoshow;
401         this.hideTooltip_(true);
402         this.showTooltip_();
403         this.tooltipAutoshown_ = markAutoshown;
404         return;
405       }
406
407       // If autoshow flag is set, make sure the tooltip gets shown.
408       if (tooltip.text && tooltip.autoshow) {
409         this.hideTooltip_(true);
410         this.showTooltip_();
411         this.tooltipAutoshown_ = true;
412         // Reset the tooltip active flag, which gets set in |showTooltip_|.
413         this.tooltipActive_ = false;
414         return;
415       }
416
417       this.hideTooltip_(true);
418
419       if (this.tooltip_)
420         this.iconElement.setAttribute('aria-lablel', this.tooltip_);
421     },
422
423     /**
424      * Sets the icon animation parameter and starts the animation.
425      * The animation is set using the resource containing all animation frames
426      * concatenated horizontally. The animator offsets the background image in
427      * regural intervals.
428      * @param {?{resourceWidth: number, frameLengthMs: number}} animation
429      *     |resourceWidth|: Total width for the resource containing the
430      *                      animation frames.
431      *     |frameLengthMs|: Time interval for which a single animation frame is
432      *                      shown.
433      */
434     setAnimation: function(animation) {
435       if (this.animationInterval_)
436         clearInterval(this.animationInterval_);
437       this.iconElement.style.backgroundPosition = 'center';
438       if (!animation)
439         return;
440       this.lastAnimationOffset_ = 0;
441       this.animationResourceSize_ = animation.resourceWidth;
442       this.animationInterval_ = setInterval(this.progressAnimation_.bind(this),
443                                             animation.frameLengthMs);
444     },
445
446     /**
447      * Sets up icon tabIndex attribute and handler for click and 'Enter' key
448      * down events.
449      * @param {?function()} callback If icon should be interactive, the
450      *     function to get called on click and 'Enter' key down events. Should
451      *     be null to make the icon  non interactive.
452      */
453     setInteractive: function(callback) {
454       // Update tabIndex property if needed.
455       if (!!this.actionHandler_ != !!callback) {
456         if (callback) {
457           this.iconElement.setAttribute('tabIndex',
458                                          UserPodTabOrder.POD_CUSTOM_ICON);
459         } else {
460           this.iconElement.removeAttribute('tabIndex');
461         }
462       }
463
464       // Set the new action handler.
465       this.actionHandler_ = callback;
466     },
467
468     /**
469      * Hides the icon. Makes sure the tooltip is hidden and animation reset.
470      */
471     hide: function() {
472       this.hideTooltip_(true);
473       this.hidden = true;
474       this.setAnimation(null);
475       this.setInteractive(null);
476       this.resetHideTransitionState_();
477     },
478
479     /**
480      * Ensures the icon's transition state potentially set by a call to
481      * {@code fadeOut} is cleared.
482      * @private
483      */
484     resetHideTransitionState_: function() {
485       if (this.hideTransitionListener_) {
486         this.iconElement.removeEventListener('webkitTransitionEnd',
487                                              this.hideTransitionListener_);
488         this.hideTransitionListener_ = null;
489       }
490       this.iconElement.classList.toggle('faded', false);
491     },
492
493     /**
494      * Handles click event on the icon element. No-op if
495      * {@code this.actionHandler_} is not set.
496      * @param {Event} e The click event.
497      * @private
498      */
499     handleClick_: function(e) {
500       if (!this.actionHandler_)
501         return;
502       this.actionHandler_();
503       stopEventPropagation(e);
504     },
505
506     /**
507      * Handles key down event on the icon element. Only 'Enter' key is handled.
508      * No-op if {@code this.actionHandler_} is not set.
509      * @param {Event} e The key down event.
510      * @private
511      */
512     handleKeyDown_: function(e) {
513       if (!this.actionHandler_ || e.keyIdentifier != 'Enter')
514         return;
515       this.actionHandler_(e);
516       stopEventPropagation(e);
517     },
518
519     /**
520      * Called when mouse enters the icon. It sets timeout for showing the
521      * tooltip.
522      * @private
523      */
524     showTooltipSoon_: function() {
525       if (this.showTooltipTimeout_ || this.tooltipActive_)
526         return;
527       this.showTooltipTimeout_ =
528           setTimeout(this.showTooltip_.bind(this), 1000);
529     },
530
531     /**
532      * Shows the current tooltip if one is set.
533      * @private
534      */
535     showTooltip_: function() {
536       if (this.hidden || !this.tooltip_ || this.tooltipActive_)
537         return;
538
539       // If autoshown bubble got hidden, clear the autoshown flag.
540       if ($('bubble').hidden && this.tooltipAutoshown_)
541         this.tooltipAutoshown_ = false;
542
543       // Show the tooltip bubble.
544       var bubbleContent = document.createElement('div');
545       bubbleContent.textContent = this.tooltip_;
546
547       /** @const */ var BUBBLE_OFFSET = CUSTOM_ICON_CONTAINER_SIZE / 2;
548       /** @const */ var BUBBLE_PADDING = 8;
549       $('bubble').showContentForElement(this,
550                                         cr.ui.Bubble.Attachment.RIGHT,
551                                         bubbleContent,
552                                         BUBBLE_OFFSET,
553                                         BUBBLE_PADDING);
554       this.ensureTooltipTimeoutCleared_();
555       this.tooltipActive_ = true;
556     },
557
558     /**
559      * Hides the tooltip. If the current tooltip was automatically shown, it
560      * will be hidden only if |force| is set.
561      * @param {boolean} Whether the tooltip should be hidden if it got shown
562      *     because autoshow flag was set.
563      * @private
564      */
565     hideTooltip_: function(force) {
566       this.tooltipActive_ = false;
567       this.ensureTooltipTimeoutCleared_();
568       if (!force && this.tooltipAutoshown_)
569         return;
570       $('bubble').hideForElement(this);
571       this.tooltipAutoshown_ = false;
572       this.iconElement.removeAttribute('aria-label');
573     },
574
575     /**
576      * Clears timaout for showing the tooltip if it's set.
577      * @private
578      */
579     ensureTooltipTimeoutCleared_: function() {
580       if (this.showTooltipTimeout_) {
581         clearTimeout(this.showTooltipTimeout_);
582         this.showTooltipTimeout_ = null;
583       }
584     },
585
586     /**
587      * Horizontally offsets the animated icon's background for a single icon
588      * size width.
589      * @private
590      */
591     progressAnimation_: function() {
592       this.lastAnimationOffset_ += this.width_;
593       if (this.lastAnimationOffset_ >= this.animationResourceSize_)
594         this.lastAnimationOffset_ = 0;
595       this.iconElement.style.backgroundPosition =
596           '-' + this.lastAnimationOffset_ + 'px center';
597     }
598   };
599
600   /**
601    * Unique salt added to user image URLs to prevent caching. Dictionary with
602    * user names as keys.
603    * @type {Object}
604    */
605   UserPod.userImageSalt_ = {};
606
607   UserPod.prototype = {
608     __proto__: HTMLDivElement.prototype,
609
610     /** @override */
611     decorate: function() {
612       this.tabIndex = UserPodTabOrder.POD_INPUT;
613       this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
614
615       this.addEventListener('keydown', this.handlePodKeyDown_.bind(this));
616       this.addEventListener('click', this.handleClickOnPod_.bind(this));
617
618       this.signinButtonElement.addEventListener('click',
619           this.activate.bind(this));
620
621       this.actionBoxAreaElement.addEventListener('mousedown',
622                                                  stopEventPropagation);
623       this.actionBoxAreaElement.addEventListener('click',
624           this.handleActionAreaButtonClick_.bind(this));
625       this.actionBoxAreaElement.addEventListener('keydown',
626           this.handleActionAreaButtonKeyDown_.bind(this));
627
628       this.actionBoxMenuRemoveElement.addEventListener('click',
629           this.handleRemoveCommandClick_.bind(this));
630       this.actionBoxMenuRemoveElement.addEventListener('keydown',
631           this.handleRemoveCommandKeyDown_.bind(this));
632       this.actionBoxMenuRemoveElement.addEventListener('blur',
633           this.handleRemoveCommandBlur_.bind(this));
634       this.actionBoxRemoveUserWarningButtonElement.addEventListener(
635           'click',
636           this.handleRemoveUserConfirmationClick_.bind(this));
637         this.actionBoxRemoveUserWarningButtonElement.addEventListener(
638             'keydown',
639             this.handleRemoveUserConfirmationKeyDown_.bind(this));
640
641       var customIcon = this.customIconElement;
642       customIcon.parentNode.replaceChild(new UserPodCustomIcon(), customIcon);
643     },
644
645     /**
646      * Initializes the pod after its properties set and added to a pod row.
647      */
648     initialize: function() {
649       this.passwordElement.addEventListener('keydown',
650           this.parentNode.handleKeyDown.bind(this.parentNode));
651       this.passwordElement.addEventListener('keypress',
652           this.handlePasswordKeyPress_.bind(this));
653
654       this.imageElement.addEventListener('load',
655           this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
656
657       var initialAuthType = this.user.initialAuthType ||
658           AUTH_TYPE.OFFLINE_PASSWORD;
659       this.setAuthType(initialAuthType, null);
660     },
661
662     /**
663      * Resets tab order for pod elements to its initial state.
664      */
665     resetTabOrder: function() {
666       // Note: the |mainInput| can be the pod itself.
667       this.mainInput.tabIndex = -1;
668       this.tabIndex = UserPodTabOrder.POD_INPUT;
669     },
670
671     /**
672      * Handles keypress event (i.e. any textual input) on password input.
673      * @param {Event} e Keypress Event object.
674      * @private
675      */
676     handlePasswordKeyPress_: function(e) {
677       // When tabbing from the system tray a tab key press is received. Suppress
678       // this so as not to type a tab character into the password field.
679       if (e.keyCode == 9) {
680         e.preventDefault();
681         return;
682       }
683     },
684
685     /**
686      * Top edge margin number of pixels.
687      * @type {?number}
688      */
689     set top(top) {
690       this.style.top = cr.ui.toCssPx(top);
691     },
692
693     /**
694      * Top edge margin number of pixels.
695      */
696     get top() {
697       return parseInt(this.style.top);
698     },
699
700     /**
701      * Left edge margin number of pixels.
702      * @type {?number}
703      */
704     set left(left) {
705       this.style.left = cr.ui.toCssPx(left);
706     },
707
708     /**
709      * Left edge margin number of pixels.
710      */
711     get left() {
712       return parseInt(this.style.left);
713     },
714
715     /**
716      * Height number of pixels.
717      */
718     get height() {
719       return this.offsetHeight;
720     },
721
722     /**
723      * Gets image element.
724      * @type {!HTMLImageElement}
725      */
726     get imageElement() {
727       return this.querySelector('.user-image');
728     },
729
730     /**
731      * Gets name element.
732      * @type {!HTMLDivElement}
733      */
734     get nameElement() {
735       return this.querySelector('.name');
736     },
737
738     /**
739      * Gets the container holding the password field.
740      * @type {!HTMLInputElement}
741      */
742     get passwordEntryContainerElement() {
743       return this.querySelector('.password-entry-container');
744     },
745
746     /**
747      * Gets password field.
748      * @type {!HTMLInputElement}
749      */
750     get passwordElement() {
751       return this.querySelector('.password');
752     },
753
754     /**
755      * Gets the password label, which is used to show a message where the
756      * password field is normally.
757      * @type {!HTMLInputElement}
758      */
759     get passwordLabelElement() {
760       return this.querySelector('.password-label');
761     },
762
763     /**
764      * Gets user sign in button.
765      * @type {!HTMLButtonElement}
766      */
767     get signinButtonElement() {
768       return this.querySelector('.signin-button');
769     },
770
771     /**
772      * Gets the container holding the launch app button.
773      * @type {!HTMLButtonElement}
774      */
775     get launchAppButtonContainerElement() {
776       return this.querySelector('.launch-app-button-container');
777     },
778
779     /**
780      * Gets launch app button.
781      * @type {!HTMLButtonElement}
782      */
783     get launchAppButtonElement() {
784       return this.querySelector('.launch-app-button');
785     },
786
787     /**
788      * Gets action box area.
789      * @type {!HTMLInputElement}
790      */
791     get actionBoxAreaElement() {
792       return this.querySelector('.action-box-area');
793     },
794
795     /**
796      * Gets user type icon area.
797      * @type {!HTMLDivElement}
798      */
799     get userTypeIconAreaElement() {
800       return this.querySelector('.user-type-icon-area');
801     },
802
803     /**
804      * Gets user type bubble like multi-profiles policy restriction message.
805      * @type {!HTMLDivElement}
806      */
807     get userTypeBubbleElement() {
808       return this.querySelector('.user-type-bubble');
809     },
810
811     /**
812      * Gets action box menu.
813      * @type {!HTMLInputElement}
814      */
815     get actionBoxMenu() {
816       return this.querySelector('.action-box-menu');
817     },
818
819     /**
820      * Gets action box menu title, user name item.
821      * @type {!HTMLInputElement}
822      */
823     get actionBoxMenuTitleNameElement() {
824       return this.querySelector('.action-box-menu-title-name');
825     },
826
827     /**
828      * Gets action box menu title, user email item.
829      * @type {!HTMLInputElement}
830      */
831     get actionBoxMenuTitleEmailElement() {
832       return this.querySelector('.action-box-menu-title-email');
833     },
834
835     /**
836      * Gets action box menu, remove user command item.
837      * @type {!HTMLInputElement}
838      */
839     get actionBoxMenuCommandElement() {
840       return this.querySelector('.action-box-menu-remove-command');
841     },
842
843     /**
844      * Gets action box menu, remove user command item div.
845      * @type {!HTMLInputElement}
846      */
847     get actionBoxMenuRemoveElement() {
848       return this.querySelector('.action-box-menu-remove');
849     },
850
851     /**
852      * Gets action box menu, remove user warning text div.
853      * @type {!HTMLInputElement}
854      */
855     get actionBoxRemoveUserWarningTextElement() {
856       return this.querySelector('.action-box-remove-user-warning-text');
857     },
858
859     /**
860      * Gets action box menu, remove supervised user warning text div.
861      * @type {!HTMLInputElement}
862      */
863     get actionBoxRemoveSupervisedUserWarningTextElement() {
864       return this.querySelector(
865           '.action-box-remove-supervised-user-warning-text');
866     },
867
868     /**
869      * Gets action box menu, remove user command item div.
870      * @type {!HTMLInputElement}
871      */
872     get actionBoxRemoveUserWarningElement() {
873       return this.querySelector('.action-box-remove-user-warning');
874     },
875
876     /**
877      * Gets action box menu, remove user command item div.
878      * @type {!HTMLInputElement}
879      */
880     get actionBoxRemoveUserWarningButtonElement() {
881       return this.querySelector('.remove-warning-button');
882     },
883
884     /**
885      * Gets the custom icon. This icon is normally hidden, but can be shown
886      * using the chrome.screenlockPrivate API.
887      * @type {!HTMLDivElement}
888      */
889     get customIconElement() {
890       return this.querySelector('.custom-icon-container');
891     },
892
893     /**
894      * Updates the user pod element.
895      */
896     update: function() {
897       this.imageElement.src = 'chrome://userimage/' + this.user.username +
898           '?id=' + UserPod.userImageSalt_[this.user.username];
899
900       this.nameElement.textContent = this.user_.displayName;
901       this.classList.toggle('signed-in', this.user_.signedIn);
902
903       if (this.isAuthTypeUserClick)
904         this.passwordLabelElement.textContent = this.authValue;
905
906       this.updateActionBoxArea();
907
908       this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
909         'passwordFieldAccessibleName', this.user_.emailAddress));
910
911       this.customizeUserPodPerUserType();
912     },
913
914     updateActionBoxArea: function() {
915       if (this.user_.publicAccount || this.user_.isApp) {
916         this.actionBoxAreaElement.hidden = true;
917         return;
918       }
919
920       this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
921
922       this.actionBoxAreaElement.setAttribute(
923           'aria-label', loadTimeData.getStringF(
924               'podMenuButtonAccessibleName', this.user_.emailAddress));
925       this.actionBoxMenuRemoveElement.setAttribute(
926           'aria-label', loadTimeData.getString(
927                'podMenuRemoveItemAccessibleName'));
928       this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
929           loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
930           this.user_.displayName;
931       this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
932       this.actionBoxMenuTitleEmailElement.hidden = this.user_.supervisedUser;
933
934       this.actionBoxMenuCommandElement.textContent =
935           loadTimeData.getString('removeUser');
936     },
937
938     customizeUserPodPerUserType: function() {
939       if (this.user_.supervisedUser && !this.user_.isDesktopUser) {
940         this.setUserPodIconType('supervised');
941       } else if (this.multiProfilesPolicyApplied) {
942         // Mark user pod as not focusable which in addition to the grayed out
943         // filter makes it look in disabled state.
944         this.classList.add('multiprofiles-policy-applied');
945         this.setUserPodIconType('policy');
946
947         if (this.user.multiProfilesPolicy == 'primary-only')
948           this.querySelector('.mp-policy-primary-only-msg').hidden = false;
949         else if (this.user.multiProfilesPolicy == 'owner-primary-only')
950           this.querySelector('.mp-owner-primary-only-msg').hidden = false;
951         else
952           this.querySelector('.mp-policy-not-allowed-msg').hidden = false;
953       } else if (this.user_.isApp) {
954         this.setUserPodIconType('app');
955       }
956     },
957
958     setUserPodIconType: function(userTypeClass) {
959       this.userTypeIconAreaElement.classList.add(userTypeClass);
960       this.userTypeIconAreaElement.hidden = false;
961     },
962
963     /**
964      * The user that this pod represents.
965      * @type {!Object}
966      */
967     user_: undefined,
968     get user() {
969       return this.user_;
970     },
971     set user(userDict) {
972       this.user_ = userDict;
973       this.update();
974     },
975
976     /**
977      * Returns true if multi-profiles sign in is currently active and this
978      * user pod is restricted per policy.
979      * @type {boolean}
980      */
981     get multiProfilesPolicyApplied() {
982       var isMultiProfilesUI =
983         (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING);
984       return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed;
985     },
986
987     /**
988      * Gets main input element.
989      * @type {(HTMLButtonElement|HTMLInputElement)}
990      */
991     get mainInput() {
992       if (this.isAuthTypePassword) {
993         return this.passwordElement;
994       } else if (this.isAuthTypeOnlineSignIn) {
995         return this.signinButtonElement;
996       } else if (this.isAuthTypeUserClick) {
997         return this;
998       }
999     },
1000
1001     /**
1002      * Whether action box button is in active state.
1003      * @type {boolean}
1004      */
1005     get isActionBoxMenuActive() {
1006       return this.actionBoxAreaElement.classList.contains('active');
1007     },
1008     set isActionBoxMenuActive(active) {
1009       if (active == this.isActionBoxMenuActive)
1010         return;
1011
1012       if (active) {
1013         this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
1014         this.actionBoxRemoveUserWarningElement.hidden = true;
1015
1016         // Clear focus first if another pod is focused.
1017         if (!this.parentNode.isFocused(this)) {
1018           this.parentNode.focusPod(undefined, true);
1019           this.actionBoxAreaElement.focus();
1020         }
1021
1022         // Hide user-type-bubble.
1023         this.userTypeBubbleElement.classList.remove('bubble-shown');
1024
1025         this.actionBoxAreaElement.classList.add('active');
1026       } else {
1027         this.actionBoxAreaElement.classList.remove('active');
1028         this.actionBoxAreaElement.classList.remove('menu-moved-up');
1029         this.actionBoxMenu.classList.remove('menu-moved-up');
1030       }
1031     },
1032
1033     /**
1034      * Whether action box button is in hovered state.
1035      * @type {boolean}
1036      */
1037     get isActionBoxMenuHovered() {
1038       return this.actionBoxAreaElement.classList.contains('hovered');
1039     },
1040     set isActionBoxMenuHovered(hovered) {
1041       if (hovered == this.isActionBoxMenuHovered)
1042         return;
1043
1044       if (hovered) {
1045         this.actionBoxAreaElement.classList.add('hovered');
1046         this.classList.add('hovered');
1047       } else {
1048         if (this.multiProfilesPolicyApplied)
1049           this.userTypeBubbleElement.classList.remove('bubble-shown');
1050         this.actionBoxAreaElement.classList.remove('hovered');
1051         this.classList.remove('hovered');
1052       }
1053     },
1054
1055     /**
1056      * Set the authentication type for the pod.
1057      * @param {number} An auth type value defined in the AUTH_TYPE enum.
1058      * @param {string} authValue The initial value used for the auth type.
1059      */
1060     setAuthType: function(authType, authValue) {
1061       this.authType_ = authType;
1062       this.authValue_ = authValue;
1063       this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]);
1064       this.update();
1065       this.reset(this.parentNode.isFocused(this));
1066     },
1067
1068     /**
1069      * The auth type of the user pod. This value is one of the enum
1070      * values in AUTH_TYPE.
1071      * @type {number}
1072      */
1073     get authType() {
1074       return this.authType_;
1075     },
1076
1077     /**
1078      * The initial value used for the pod's authentication type.
1079      * eg. a prepopulated password input when using password authentication.
1080      */
1081     get authValue() {
1082       return this.authValue_;
1083     },
1084
1085     /**
1086      * True if the the user pod uses a password to authenticate.
1087      * @type {bool}
1088      */
1089     get isAuthTypePassword() {
1090       return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD ||
1091              this.authType_ == AUTH_TYPE.FORCE_OFFLINE_PASSWORD;
1092     },
1093
1094     /**
1095      * True if the the user pod uses a user click to authenticate.
1096      * @type {bool}
1097      */
1098     get isAuthTypeUserClick() {
1099       return this.authType_ == AUTH_TYPE.USER_CLICK;
1100     },
1101
1102     /**
1103      * True if the the user pod uses a online sign in to authenticate.
1104      * @type {bool}
1105      */
1106     get isAuthTypeOnlineSignIn() {
1107       return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN;
1108     },
1109
1110     /**
1111      * Updates the image element of the user.
1112      */
1113     updateUserImage: function() {
1114       UserPod.userImageSalt_[this.user.username] = new Date().getTime();
1115       this.update();
1116     },
1117
1118     /**
1119      * Focuses on input element.
1120      */
1121     focusInput: function() {
1122       // Move tabIndex from the whole pod to the main input.
1123       // Note: the |mainInput| can be the pod itself.
1124       this.tabIndex = -1;
1125       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1126       this.mainInput.focus();
1127     },
1128
1129     /**
1130      * Activates the pod.
1131      * @param {Event} e Event object.
1132      * @return {boolean} True if activated successfully.
1133      */
1134     activate: function(e) {
1135       if (this.isAuthTypeOnlineSignIn) {
1136         this.showSigninUI();
1137       } else if (this.isAuthTypeUserClick) {
1138         Oobe.disableSigninUI();
1139         chrome.send('attemptUnlock', [this.user.username]);
1140       } else if (this.isAuthTypePassword) {
1141         if (!this.passwordElement.value)
1142           return false;
1143         Oobe.disableSigninUI();
1144         chrome.send('authenticateUser',
1145                     [this.user.username, this.passwordElement.value]);
1146       } else {
1147         console.error('Activating user pod with invalid authentication type: ' +
1148             this.authType);
1149       }
1150
1151       return true;
1152     },
1153
1154     showSupervisedUserSigninWarning: function() {
1155       // Supervised user token has been invalidated.
1156       // Make sure that pod is focused i.e. "Sign in" button is seen.
1157       this.parentNode.focusPod(this);
1158
1159       var error = document.createElement('div');
1160       var messageDiv = document.createElement('div');
1161       messageDiv.className = 'error-message-bubble';
1162       messageDiv.textContent =
1163           loadTimeData.getString('supervisedUserExpiredTokenWarning');
1164       error.appendChild(messageDiv);
1165
1166       $('bubble').showContentForElement(
1167           this.signinButtonElement,
1168           cr.ui.Bubble.Attachment.TOP,
1169           error,
1170           this.signinButtonElement.offsetWidth / 2,
1171           4);
1172       // Move warning bubble up if it overlaps the shelf.
1173       var maxHeight =
1174           cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping($('bubble'));
1175       if (maxHeight < $('bubble').offsetHeight) {
1176         $('bubble').showContentForElement(
1177             this.signinButtonElement,
1178             cr.ui.Bubble.Attachment.BOTTOM,
1179             error,
1180             this.signinButtonElement.offsetWidth / 2,
1181             4);
1182       }
1183     },
1184
1185     /**
1186      * Shows signin UI for this user.
1187      */
1188     showSigninUI: function() {
1189       if (this.user.supervisedUser && !this.user.isDesktopUser) {
1190         this.showSupervisedUserSigninWarning();
1191       } else {
1192         // Special case for multi-profiles sign in. We show users even if they
1193         // are not allowed per policy. Restrict those users from starting GAIA.
1194         if (this.multiProfilesPolicyApplied)
1195           return;
1196
1197         this.parentNode.showSigninUI(this.user.emailAddress);
1198       }
1199     },
1200
1201     /**
1202      * Resets the input field and updates the tab order of pod controls.
1203      * @param {boolean} takeFocus If true, input field takes focus.
1204      */
1205     reset: function(takeFocus) {
1206       this.passwordElement.value = '';
1207       if (takeFocus) {
1208         if (!this.multiProfilesPolicyApplied)
1209           this.focusInput();  // This will set a custom tab order.
1210       }
1211       else
1212         this.resetTabOrder();
1213     },
1214
1215     /**
1216      * Removes a user using the correct identifier based on user type.
1217      * @param {Object} user User to be removed.
1218      */
1219     removeUser: function(user) {
1220       chrome.send('removeUser',
1221                   [user.isDesktopUser ? user.profilePath : user.username]);
1222     },
1223
1224     /**
1225      * Handles a click event on action area button.
1226      * @param {Event} e Click event.
1227      */
1228     handleActionAreaButtonClick_: function(e) {
1229       if (this.parentNode.disabled)
1230         return;
1231       this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
1232       e.stopPropagation();
1233     },
1234
1235     /**
1236      * Handles a keydown event on action area button.
1237      * @param {Event} e KeyDown event.
1238      */
1239     handleActionAreaButtonKeyDown_: function(e) {
1240       if (this.disabled)
1241         return;
1242       switch (e.keyIdentifier) {
1243         case 'Enter':
1244         case 'U+0020':  // Space
1245           if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
1246             this.isActionBoxMenuActive = true;
1247           e.stopPropagation();
1248           break;
1249         case 'Up':
1250         case 'Down':
1251           if (this.isActionBoxMenuActive) {
1252             this.actionBoxMenuRemoveElement.tabIndex =
1253                 UserPodTabOrder.PAD_MENU_ITEM;
1254             this.actionBoxMenuRemoveElement.focus();
1255           }
1256           e.stopPropagation();
1257           break;
1258         case 'U+001B':  // Esc
1259           this.isActionBoxMenuActive = false;
1260           e.stopPropagation();
1261           break;
1262         case 'U+0009':  // Tab
1263           if (!this.parentNode.alwaysFocusSinglePod)
1264             this.parentNode.focusPod();
1265         default:
1266           this.isActionBoxMenuActive = false;
1267           break;
1268       }
1269     },
1270
1271     /**
1272      * Handles a click event on remove user command.
1273      * @param {Event} e Click event.
1274      */
1275     handleRemoveCommandClick_: function(e) {
1276       if (this.user.supervisedUser || this.user.isDesktopUser) {
1277         this.showRemoveWarning_();
1278         return;
1279       }
1280       if (this.isActionBoxMenuActive)
1281         chrome.send('removeUser', [this.user.username]);
1282     },
1283
1284     /**
1285      * Shows remove user warning. Used for supervised users on CrOS, and for all
1286      * users on desktop.
1287      */
1288     showRemoveWarning_: function() {
1289       this.actionBoxMenuRemoveElement.hidden = true;
1290       this.actionBoxRemoveUserWarningElement.hidden = false;
1291       this.actionBoxRemoveUserWarningButtonElement.focus();
1292
1293       // Move up the menu if it overlaps shelf.
1294       var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(
1295           this.actionBoxMenu);
1296       var actualHeight = parseInt(
1297           window.getComputedStyle(this.actionBoxMenu).height);
1298       if (maxHeight < actualHeight) {
1299         this.actionBoxMenu.classList.add('menu-moved-up');
1300         this.actionBoxAreaElement.classList.add('menu-moved-up');
1301       }
1302     },
1303
1304     /**
1305      * Handles a click event on remove user confirmation button.
1306      * @param {Event} e Click event.
1307      */
1308     handleRemoveUserConfirmationClick_: function(e) {
1309       if (this.isActionBoxMenuActive) {
1310         this.isActionBoxMenuActive = false;
1311         this.removeUser(this.user);
1312         e.stopPropagation();
1313       }
1314     },
1315
1316     /**
1317      * Handles a keydown event on remove user confirmation button.
1318      * @param {Event} e KeyDown event.
1319      */
1320     handleRemoveUserConfirmationKeyDown_: function(e) {
1321       if (!this.isActionBoxMenuActive)
1322         return;
1323
1324       // Only handle pressing 'Enter' or 'Space', and let all other events
1325       // bubble to the action box menu.
1326       if (e.keyIdentifier == 'Enter' || e.keyIdentifier == 'U+0020') {
1327         this.isActionBoxMenuActive = false;
1328         this.removeUser(this.user);
1329         e.stopPropagation();
1330         // Prevent default so that we don't trigger a 'click' event.
1331         e.preventDefault();
1332       }
1333     },
1334
1335     /**
1336      * Handles a keydown event on remove command.
1337      * @param {Event} e KeyDown event.
1338      */
1339     handleRemoveCommandKeyDown_: function(e) {
1340       if (this.disabled)
1341         return;
1342       switch (e.keyIdentifier) {
1343         case 'Enter':
1344           if (this.user.supervisedUser || this.user.isDesktopUser) {
1345             // Prevent default so that we don't trigger a 'click' event on the
1346             // remove button that will be focused.
1347             e.preventDefault();
1348             this.showRemoveWarning_();
1349           } else {
1350             this.removeUser(this.user);
1351           }
1352           e.stopPropagation();
1353           break;
1354         case 'Up':
1355         case 'Down':
1356           e.stopPropagation();
1357           break;
1358         case 'U+001B':  // Esc
1359           this.actionBoxAreaElement.focus();
1360           this.isActionBoxMenuActive = false;
1361           e.stopPropagation();
1362           break;
1363         default:
1364           this.actionBoxAreaElement.focus();
1365           this.isActionBoxMenuActive = false;
1366           break;
1367       }
1368     },
1369
1370     /**
1371      * Handles a blur event on remove command.
1372      * @param {Event} e Blur event.
1373      */
1374     handleRemoveCommandBlur_: function(e) {
1375       if (this.disabled)
1376         return;
1377       this.actionBoxMenuRemoveElement.tabIndex = -1;
1378     },
1379
1380     /**
1381      * Handles click event on a user pod.
1382      * @param {Event} e Click event.
1383      */
1384     handleClickOnPod_: function(e) {
1385       if (this.parentNode.disabled)
1386         return;
1387
1388       if (!this.isActionBoxMenuActive) {
1389         if (this.isAuthTypeOnlineSignIn) {
1390           this.showSigninUI();
1391         } else if (this.isAuthTypeUserClick) {
1392           this.parentNode.setActivatedPod(this);
1393         }
1394
1395         if (this.multiProfilesPolicyApplied)
1396           this.userTypeBubbleElement.classList.add('bubble-shown');
1397
1398         // Prevent default so that we don't trigger 'focus' event.
1399         e.preventDefault();
1400       }
1401     },
1402
1403     /**
1404      * Handles keydown event for a user pod.
1405      * @param {Event} e Key event.
1406      */
1407     handlePodKeyDown_: function(e) {
1408       if (!this.isAuthTypeUserClick || this.disabled)
1409         return;
1410       switch (e.keyIdentifier) {
1411         case 'Enter':
1412         case 'U+0020':  // Space
1413           if (this.parentNode.isFocused(this))
1414             this.parentNode.setActivatedPod(this);
1415           break;
1416       }
1417     }
1418   };
1419
1420   /**
1421    * Creates a public account user pod.
1422    * @constructor
1423    * @extends {UserPod}
1424    */
1425   var PublicAccountUserPod = cr.ui.define(function() {
1426     var node = UserPod();
1427
1428     var extras = $('public-account-user-pod-extras-template').children;
1429     for (var i = 0; i < extras.length; ++i) {
1430       var el = extras[i].cloneNode(true);
1431       node.appendChild(el);
1432     }
1433
1434     return node;
1435   });
1436
1437   PublicAccountUserPod.prototype = {
1438     __proto__: UserPod.prototype,
1439
1440     /**
1441      * "Enter" button in expanded side pane.
1442      * @type {!HTMLButtonElement}
1443      */
1444     get enterButtonElement() {
1445       return this.querySelector('.enter-button');
1446     },
1447
1448     /**
1449      * Boolean flag of whether the pod is showing the side pane. The flag
1450      * controls whether 'expanded' class is added to the pod's class list and
1451      * resets tab order because main input element changes when the 'expanded'
1452      * state changes.
1453      * @type {boolean}
1454      */
1455     get expanded() {
1456       return this.classList.contains('expanded');
1457     },
1458
1459     set expanded(expanded) {
1460       if (this.expanded == expanded)
1461         return;
1462
1463       this.resetTabOrder();
1464       this.classList.toggle('expanded', expanded);
1465       if (expanded) {
1466         // Show the advanced expanded pod directly if there are at least two
1467         // recommended locales. This will be the case in multilingual
1468         // environments where users are likely to want to choose among locales.
1469         if (this.querySelector('.language-select').multipleRecommendedLocales)
1470           this.classList.add('advanced');
1471         this.usualLeft = this.left;
1472         this.makeSpaceForExpandedPod_();
1473       } else if (typeof(this.usualLeft) != 'undefined') {
1474         this.left = this.usualLeft;
1475       }
1476
1477       var self = this;
1478       this.classList.add('animating');
1479       this.addEventListener('webkitTransitionEnd', function f(e) {
1480         self.removeEventListener('webkitTransitionEnd', f);
1481         self.classList.remove('animating');
1482
1483         // Accessibility focus indicator does not move with the focused
1484         // element. Sends a 'focus' event on the currently focused element
1485         // so that accessibility focus indicator updates its location.
1486         if (document.activeElement)
1487           document.activeElement.dispatchEvent(new Event('focus'));
1488       });
1489       // Guard timer set to animation duration + 20ms.
1490       ensureTransitionEndEvent(this, 200);
1491     },
1492
1493     get advanced() {
1494       return this.classList.contains('advanced');
1495     },
1496
1497     /** @override */
1498     get mainInput() {
1499       if (this.expanded)
1500         return this.enterButtonElement;
1501       else
1502         return this.nameElement;
1503     },
1504
1505     /** @override */
1506     decorate: function() {
1507       UserPod.prototype.decorate.call(this);
1508
1509       this.classList.add('public-account');
1510
1511       this.nameElement.addEventListener('keydown', (function(e) {
1512         if (e.keyIdentifier == 'Enter') {
1513           this.parentNode.setActivatedPod(this, e);
1514           // Stop this keydown event from bubbling up to PodRow handler.
1515           e.stopPropagation();
1516           // Prevent default so that we don't trigger a 'click' event on the
1517           // newly focused "Enter" button.
1518           e.preventDefault();
1519         }
1520       }).bind(this));
1521
1522       var learnMore = this.querySelector('.learn-more');
1523       learnMore.addEventListener('mousedown', stopEventPropagation);
1524       learnMore.addEventListener('click', this.handleLearnMoreEvent);
1525       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1526
1527       learnMore = this.querySelector('.expanded-pane-learn-more');
1528       learnMore.addEventListener('click', this.handleLearnMoreEvent);
1529       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1530
1531       var languageSelect = this.querySelector('.language-select');
1532       languageSelect.tabIndex = UserPodTabOrder.POD_INPUT;
1533       languageSelect.manuallyChanged = false;
1534       languageSelect.addEventListener(
1535           'change',
1536           function() {
1537             languageSelect.manuallyChanged = true;
1538             this.getPublicSessionKeyboardLayouts_();
1539           }.bind(this));
1540
1541       var keyboardSelect = this.querySelector('.keyboard-select');
1542       keyboardSelect.tabIndex = UserPodTabOrder.POD_INPUT;
1543       keyboardSelect.loadedLocale = null;
1544
1545       var languageAndInput = this.querySelector('.language-and-input');
1546       languageAndInput.tabIndex = UserPodTabOrder.POD_INPUT;
1547       languageAndInput.addEventListener('click',
1548                                         this.transitionToAdvanced_.bind(this));
1549
1550       this.enterButtonElement.addEventListener('click', (function(e) {
1551         this.enterButtonElement.disabled = true;
1552         var locale = this.querySelector('.language-select').value;
1553         var keyboardSelect = this.querySelector('.keyboard-select');
1554         // The contents of |keyboardSelect| is updated asynchronously. If its
1555         // locale does not match |locale|, it has not updated yet and the
1556         // currently selected keyboard layout may not be applicable to |locale|.
1557         // Do not return any keyboard layout in this case and let the backend
1558         // choose a suitable layout.
1559         var keyboardLayout =
1560             keyboardSelect.loadedLocale == locale ? keyboardSelect.value : '';
1561         chrome.send('launchPublicSession',
1562                     [this.user.username, locale, keyboardLayout]);
1563       }).bind(this));
1564     },
1565
1566     /** @override **/
1567     initialize: function() {
1568       UserPod.prototype.initialize.call(this);
1569
1570       id = this.user.username + '-keyboard';
1571       this.querySelector('.keyboard-select-label').htmlFor = id;
1572       this.querySelector('.keyboard-select').setAttribute('id', id);
1573
1574       var id = this.user.username + '-language';
1575       this.querySelector('.language-select-label').htmlFor = id;
1576       var languageSelect = this.querySelector('.language-select');
1577       languageSelect.setAttribute('id', id);
1578       this.populateLanguageSelect(this.user.initialLocales,
1579                                   this.user.initialLocale,
1580                                   this.user.initialMultipleRecommendedLocales);
1581     },
1582
1583     /** @override **/
1584     update: function() {
1585       UserPod.prototype.update.call(this);
1586       this.querySelector('.expanded-pane-name').textContent =
1587           this.user_.displayName;
1588       this.querySelector('.info').textContent =
1589           loadTimeData.getStringF('publicAccountInfoFormat',
1590                                   this.user_.enterpriseDomain);
1591     },
1592
1593     /** @override */
1594     focusInput: function() {
1595       // Move tabIndex from the whole pod to the main input.
1596       this.tabIndex = -1;
1597       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1598       this.mainInput.focus();
1599     },
1600
1601     /** @override */
1602     reset: function(takeFocus) {
1603       if (!takeFocus)
1604         this.expanded = false;
1605       this.enterButtonElement.disabled = false;
1606       UserPod.prototype.reset.call(this, takeFocus);
1607     },
1608
1609     /** @override */
1610     activate: function(e) {
1611       if (!this.expanded) {
1612         this.expanded = true;
1613         this.focusInput();
1614       }
1615       return true;
1616     },
1617
1618     /** @override */
1619     handleClickOnPod_: function(e) {
1620       if (this.parentNode.disabled)
1621         return;
1622
1623       this.parentNode.focusPod(this);
1624       this.parentNode.setActivatedPod(this, e);
1625       // Prevent default so that we don't trigger 'focus' event.
1626       e.preventDefault();
1627     },
1628
1629     /**
1630      * Updates the display name shown on the pod.
1631      * @param {string} displayName The new display name
1632      */
1633     setDisplayName: function(displayName) {
1634       this.user_.displayName = displayName;
1635       this.update();
1636     },
1637
1638     /**
1639      * Handle mouse and keyboard events for the learn more button. Triggering
1640      * the button causes information about public sessions to be shown.
1641      * @param {Event} event Mouse or keyboard event.
1642      */
1643     handleLearnMoreEvent: function(event) {
1644       switch (event.type) {
1645         // Show informaton on left click. Let any other clicks propagate.
1646         case 'click':
1647           if (event.button != 0)
1648             return;
1649           break;
1650         // Show informaton when <Return> or <Space> is pressed. Let any other
1651         // key presses propagate.
1652         case 'keydown':
1653           switch (event.keyCode) {
1654             case 13:  // Return.
1655             case 32:  // Space.
1656               break;
1657             default:
1658               return;
1659           }
1660           break;
1661       }
1662       chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
1663       stopEventPropagation(event);
1664     },
1665
1666     makeSpaceForExpandedPod_: function() {
1667       var width = this.classList.contains('advanced') ?
1668           PUBLIC_EXPANDED_ADVANCED_WIDTH : PUBLIC_EXPANDED_BASIC_WIDTH;
1669       var isDesktopUserManager = Oobe.getInstance().displayType ==
1670           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1671       var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
1672                                               POD_ROW_PADDING;
1673       if (this.left + width > $('pod-row').offsetWidth - rowPadding)
1674         this.left = $('pod-row').offsetWidth - rowPadding - width;
1675     },
1676
1677     /**
1678      * Transition the expanded pod from the basic to the advanced view.
1679      */
1680     transitionToAdvanced_: function() {
1681       var pod = this;
1682       var languageAndInputSection =
1683           this.querySelector('.language-and-input-section');
1684       this.classList.add('transitioning-to-advanced');
1685       setTimeout(function() {
1686         pod.classList.add('advanced');
1687         pod.makeSpaceForExpandedPod_();
1688         languageAndInputSection.addEventListener('webkitTransitionEnd',
1689                                                  function observer() {
1690           languageAndInputSection.removeEventListener('webkitTransitionEnd',
1691                                                       observer);
1692           pod.classList.remove('transitioning-to-advanced');
1693           pod.querySelector('.language-select').focus();
1694         });
1695         // Guard timer set to animation duration + 20ms.
1696         ensureTransitionEndEvent(languageAndInputSection, 380);
1697       }, 0);
1698     },
1699
1700     /**
1701      * Retrieves the list of keyboard layouts available for the currently
1702      * selected locale.
1703      */
1704     getPublicSessionKeyboardLayouts_: function() {
1705       var selectedLocale = this.querySelector('.language-select').value;
1706       if (selectedLocale ==
1707           this.querySelector('.keyboard-select').loadedLocale) {
1708         // If the list of keyboard layouts was loaded for the currently selected
1709         // locale, it is already up to date.
1710         return;
1711       }
1712       chrome.send('getPublicSessionKeyboardLayouts',
1713                   [this.user.username, selectedLocale]);
1714      },
1715
1716     /**
1717      * Populates the keyboard layout "select" element with a list of layouts.
1718      * @param {string} locale The locale to which this list of keyboard layouts
1719      *     applies
1720      * @param {!Object} list List of available keyboard layouts
1721      */
1722     populateKeyboardSelect: function(locale, list) {
1723       if (locale != this.querySelector('.language-select').value) {
1724         // The selected locale has changed and the list of keyboard layouts is
1725         // not applicable. This method will be called again when a list of
1726         // keyboard layouts applicable to the selected locale is retrieved.
1727         return;
1728       }
1729
1730       var keyboardSelect = this.querySelector('.keyboard-select');
1731       keyboardSelect.loadedLocale = locale;
1732       keyboardSelect.innerHTML = '';
1733       for (var i = 0; i < list.length; ++i) {
1734         var item = list[i];
1735         keyboardSelect.appendChild(
1736             new Option(item.title, item.value, item.selected, item.selected));
1737       }
1738     },
1739
1740     /**
1741      * Populates the language "select" element with a list of locales.
1742      * @param {!Object} locales The list of available locales
1743      * @param {string} defaultLocale The locale to select by default
1744      * @param {boolean} multipleRecommendedLocales Whether |locales| contains
1745      *     two or more recommended locales
1746      */
1747     populateLanguageSelect: function(locales,
1748                                      defaultLocale,
1749                                      multipleRecommendedLocales) {
1750       var languageSelect = this.querySelector('.language-select');
1751       // If the user manually selected a locale, do not change the selection.
1752       // Otherwise, select the new |defaultLocale|.
1753       var selected =
1754           languageSelect.manuallyChanged ? languageSelect.value : defaultLocale;
1755       languageSelect.innerHTML = '';
1756       var group = languageSelect;
1757       for (var i = 0; i < locales.length; ++i) {
1758         var item = locales[i];
1759         if (item.optionGroupName) {
1760           group = document.createElement('optgroup');
1761           group.label = item.optionGroupName;
1762           languageSelect.appendChild(group);
1763         } else {
1764           group.appendChild(new Option(item.title,
1765                                        item.value,
1766                                        item.value == selected,
1767                                        item.value == selected));
1768         }
1769       }
1770       languageSelect.multipleRecommendedLocales = multipleRecommendedLocales;
1771
1772       // Retrieve a list of keyboard layouts applicable to the locale that is
1773       // now selected.
1774       this.getPublicSessionKeyboardLayouts_();
1775     }
1776   };
1777
1778   /**
1779    * Creates a user pod to be used only in desktop chrome.
1780    * @constructor
1781    * @extends {UserPod}
1782    */
1783   var DesktopUserPod = cr.ui.define(function() {
1784     // Don't just instantiate a UserPod(), as this will call decorate() on the
1785     // parent object, and add duplicate event listeners.
1786     var node = $('user-pod-template').cloneNode(true);
1787     node.removeAttribute('id');
1788     return node;
1789   });
1790
1791   DesktopUserPod.prototype = {
1792     __proto__: UserPod.prototype,
1793
1794     /** @override */
1795     get mainInput() {
1796       if (this.user.needsSignin)
1797         return this.passwordElement;
1798       else
1799         return this.nameElement;
1800     },
1801
1802     /** @override */
1803     update: function() {
1804       this.imageElement.src = this.user.userImage;
1805       this.nameElement.textContent = this.user.displayName;
1806
1807       var isLockedUser = this.user.needsSignin;
1808       var isSupervisedUser = this.user.supervisedUser;
1809       this.classList.toggle('locked', isLockedUser);
1810       this.classList.toggle('supervised-user', isSupervisedUser);
1811
1812       if (this.isAuthTypeUserClick)
1813         this.passwordLabelElement.textContent = this.authValue;
1814
1815       this.actionBoxRemoveUserWarningTextElement.hidden = isSupervisedUser;
1816       this.actionBoxRemoveSupervisedUserWarningTextElement.hidden =
1817           !isSupervisedUser;
1818
1819       UserPod.prototype.updateActionBoxArea.call(this);
1820     },
1821
1822     /** @override */
1823     focusInput: function() {
1824       // Move tabIndex from the whole pod to the main input.
1825       this.tabIndex = -1;
1826       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1827       this.mainInput.focus();
1828     },
1829
1830     /** @override */
1831     activate: function(e) {
1832       if (!this.user.needsSignin) {
1833         Oobe.launchUser(this.user.emailAddress, this.user.displayName);
1834       } else if (!this.passwordElement.value) {
1835         return false;
1836       } else {
1837         chrome.send('authenticatedLaunchUser',
1838                     [this.user.emailAddress,
1839                      this.user.displayName,
1840                      this.passwordElement.value]);
1841       }
1842       this.passwordElement.value = '';
1843       return true;
1844     },
1845
1846     /** @override */
1847     handleClickOnPod_: function(e) {
1848       if (this.parentNode.disabled)
1849         return;
1850
1851       Oobe.clearErrors();
1852       this.parentNode.lastFocusedPod_ = this;
1853
1854       // If this is an unlocked pod, then open a browser window. Otherwise
1855       // just activate the pod and show the password field.
1856       if (!this.user.needsSignin && !this.isActionBoxMenuActive)
1857         this.activate(e);
1858
1859       if (this.isAuthTypeUserClick)
1860         chrome.send('attemptUnlock', [this.user.emailAddress]);
1861     },
1862   };
1863
1864   /**
1865    * Creates a user pod that represents kiosk app.
1866    * @constructor
1867    * @extends {UserPod}
1868    */
1869   var KioskAppPod = cr.ui.define(function() {
1870     var node = UserPod();
1871     return node;
1872   });
1873
1874   KioskAppPod.prototype = {
1875     __proto__: UserPod.prototype,
1876
1877     /** @override */
1878     decorate: function() {
1879       UserPod.prototype.decorate.call(this);
1880       this.launchAppButtonElement.addEventListener('click',
1881                                                    this.activate.bind(this));
1882     },
1883
1884     /** @override */
1885     update: function() {
1886       this.imageElement.src = this.user.iconUrl;
1887       this.imageElement.alt = this.user.label;
1888       this.imageElement.title = this.user.label;
1889       this.passwordEntryContainerElement.hidden = true;
1890       this.launchAppButtonContainerElement.hidden = false;
1891       this.nameElement.textContent = this.user.label;
1892
1893       UserPod.prototype.updateActionBoxArea.call(this);
1894       UserPod.prototype.customizeUserPodPerUserType.call(this);
1895     },
1896
1897     /** @override */
1898     get mainInput() {
1899       return this.launchAppButtonElement;
1900     },
1901
1902     /** @override */
1903     focusInput: function() {
1904       // Move tabIndex from the whole pod to the main input.
1905       this.tabIndex = -1;
1906       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1907       this.mainInput.focus();
1908     },
1909
1910     /** @override */
1911     get forceOnlineSignin() {
1912       return false;
1913     },
1914
1915     /** @override */
1916     activate: function(e) {
1917       var diagnosticMode = e && e.ctrlKey;
1918       this.launchApp_(this.user, diagnosticMode);
1919       return true;
1920     },
1921
1922     /** @override */
1923     handleClickOnPod_: function(e) {
1924       if (this.parentNode.disabled)
1925         return;
1926
1927       Oobe.clearErrors();
1928       this.parentNode.lastFocusedPod_ = this;
1929       this.activate(e);
1930     },
1931
1932     /**
1933      * Launch the app. If |diagnosticMode| is true, ask user to confirm.
1934      * @param {Object} app App data.
1935      * @param {boolean} diagnosticMode Whether to run the app in diagnostic
1936      *     mode.
1937      */
1938     launchApp_: function(app, diagnosticMode) {
1939       if (!diagnosticMode) {
1940         chrome.send('launchKioskApp', [app.id, false]);
1941         return;
1942       }
1943
1944       var oobe = $('oobe');
1945       if (!oobe.confirmDiagnosticMode_) {
1946         oobe.confirmDiagnosticMode_ =
1947             new cr.ui.dialogs.ConfirmDialog(document.body);
1948         oobe.confirmDiagnosticMode_.setOkLabel(
1949             loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
1950         oobe.confirmDiagnosticMode_.setCancelLabel(
1951             loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
1952       }
1953
1954       oobe.confirmDiagnosticMode_.show(
1955           loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
1956                                   app.label),
1957           function() {
1958             chrome.send('launchKioskApp', [app.id, true]);
1959           });
1960     },
1961   };
1962
1963   /**
1964    * Creates a new pod row element.
1965    * @constructor
1966    * @extends {HTMLDivElement}
1967    */
1968   var PodRow = cr.ui.define('podrow');
1969
1970   PodRow.prototype = {
1971     __proto__: HTMLDivElement.prototype,
1972
1973     // Whether this user pod row is shown for the first time.
1974     firstShown_: true,
1975
1976     // True if inside focusPod().
1977     insideFocusPod_: false,
1978
1979     // Focused pod.
1980     focusedPod_: undefined,
1981
1982     // Activated pod, i.e. the pod of current login attempt.
1983     activatedPod_: undefined,
1984
1985     // Pod that was most recently focused, if any.
1986     lastFocusedPod_: undefined,
1987
1988     // Pods whose initial images haven't been loaded yet.
1989     podsWithPendingImages_: [],
1990
1991     // Whether pod placement has been postponed.
1992     podPlacementPostponed_: false,
1993
1994     // Standard user pod height/width.
1995     userPodHeight_: 0,
1996     userPodWidth_: 0,
1997
1998     // Array of apps that are shown in addition to other user pods.
1999     apps_: [],
2000
2001     // True to show app pods along with user pods.
2002     shouldShowApps_: true,
2003
2004     // Array of users that are shown (public/supervised/regular).
2005     users_: [],
2006
2007     /** @override */
2008     decorate: function() {
2009       // Event listeners that are installed for the time period during which
2010       // the element is visible.
2011       this.listeners_ = {
2012         focus: [this.handleFocus_.bind(this), true /* useCapture */],
2013         click: [this.handleClick_.bind(this), true],
2014         mousemove: [this.handleMouseMove_.bind(this), false],
2015         keydown: [this.handleKeyDown.bind(this), false]
2016       };
2017
2018       var isDesktopUserManager = Oobe.getInstance().displayType ==
2019           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2020       this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT :
2021                                                    CROS_POD_HEIGHT;
2022       // Same for Chrome OS and desktop.
2023       this.userPodWidth_ = POD_WIDTH;
2024     },
2025
2026     /**
2027      * Returns all the pods in this pod row.
2028      * @type {NodeList}
2029      */
2030     get pods() {
2031       return Array.prototype.slice.call(this.children);
2032     },
2033
2034     /**
2035      * Return true if user pod row has only single user pod in it, which should
2036      * always be focused.
2037      * @type {boolean}
2038      */
2039     get alwaysFocusSinglePod() {
2040       var isDesktopUserManager = Oobe.getInstance().displayType ==
2041           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2042
2043       return isDesktopUserManager ? false : this.children.length == 1;
2044     },
2045
2046     /**
2047      * Returns pod with the given app id.
2048      * @param {!string} app_id Application id to be matched.
2049      * @return {Object} Pod with the given app id. null if pod hasn't been
2050      *     found.
2051      */
2052     getPodWithAppId_: function(app_id) {
2053       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2054         if (pod.user.isApp && pod.user.id == app_id)
2055           return pod;
2056       }
2057       return null;
2058     },
2059
2060     /**
2061      * Returns pod with the given username (null if there is no such pod).
2062      * @param {string} username Username to be matched.
2063      * @return {Object} Pod with the given username. null if pod hasn't been
2064      *     found.
2065      */
2066     getPodWithUsername_: function(username) {
2067       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2068         if (pod.user.username == username)
2069           return pod;
2070       }
2071       return null;
2072     },
2073
2074     /**
2075      * True if the the pod row is disabled (handles no user interaction).
2076      * @type {boolean}
2077      */
2078     disabled_: false,
2079     get disabled() {
2080       return this.disabled_;
2081     },
2082     set disabled(value) {
2083       this.disabled_ = value;
2084       var controls = this.querySelectorAll('button,input');
2085       for (var i = 0, control; control = controls[i]; ++i) {
2086         control.disabled = value;
2087       }
2088     },
2089
2090     /**
2091      * Creates a user pod from given email.
2092      * @param {!Object} user User info dictionary.
2093      */
2094     createUserPod: function(user) {
2095       var userPod;
2096       if (user.isDesktopUser)
2097         userPod = new DesktopUserPod({user: user});
2098       else if (user.publicAccount)
2099         userPod = new PublicAccountUserPod({user: user});
2100       else if (user.isApp)
2101         userPod = new KioskAppPod({user: user});
2102       else
2103         userPod = new UserPod({user: user});
2104
2105       userPod.hidden = false;
2106       return userPod;
2107     },
2108
2109     /**
2110      * Add an existing user pod to this pod row.
2111      * @param {!Object} user User info dictionary.
2112      */
2113     addUserPod: function(user) {
2114       var userPod = this.createUserPod(user);
2115       this.appendChild(userPod);
2116       userPod.initialize();
2117     },
2118
2119     /**
2120      * Runs app with a given id from the list of loaded apps.
2121      * @param {!string} app_id of an app to run.
2122      * @param {boolean=} opt_diagnostic_mode Whether to run the app in
2123      *     diagnostic mode. Default is false.
2124      */
2125     findAndRunAppForTesting: function(app_id, opt_diagnostic_mode) {
2126       var app = this.getPodWithAppId_(app_id);
2127       if (app) {
2128         var activationEvent = cr.doc.createEvent('MouseEvents');
2129         var ctrlKey = opt_diagnostic_mode;
2130         activationEvent.initMouseEvent('click', true, true, null,
2131             0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
2132         app.dispatchEvent(activationEvent);
2133       }
2134     },
2135
2136     /**
2137      * Removes user pod from pod row.
2138      * @param {string} email User's email.
2139      */
2140     removeUserPod: function(username) {
2141       var podToRemove = this.getPodWithUsername_(username);
2142       if (podToRemove == null) {
2143         console.warn('Attempt to remove not existing pod for ' + username +
2144             '.');
2145         return;
2146       }
2147       this.removeChild(podToRemove);
2148       if (this.pods.length > 0)
2149         this.placePods_();
2150     },
2151
2152     /**
2153      * Returns index of given pod or -1 if not found.
2154      * @param {UserPod} pod Pod to look up.
2155      * @private
2156      */
2157     indexOf_: function(pod) {
2158       for (var i = 0; i < this.pods.length; ++i) {
2159         if (pod == this.pods[i])
2160           return i;
2161       }
2162       return -1;
2163     },
2164
2165     /**
2166      * Populates pod row with given existing users and start init animation.
2167      * @param {array} users Array of existing user emails.
2168      */
2169     loadPods: function(users) {
2170       this.users_ = users;
2171
2172       this.rebuildPods();
2173     },
2174
2175     /**
2176      * Scrolls focused user pod into view.
2177      */
2178     scrollFocusedPodIntoView: function() {
2179       var pod = this.focusedPod_;
2180       if (!pod)
2181         return;
2182
2183       // First check whether focused pod is already fully visible.
2184       var visibleArea = $('scroll-container');
2185       var scrollTop = visibleArea.scrollTop;
2186       var clientHeight = visibleArea.clientHeight;
2187       var podTop = $('oobe').offsetTop + pod.offsetTop;
2188       var padding = USER_POD_KEYBOARD_MIN_PADDING;
2189       if (podTop + pod.height + padding <= scrollTop + clientHeight &&
2190           podTop - padding >= scrollTop) {
2191         return;
2192       }
2193
2194       // Scroll so that user pod is as centered as possible.
2195       visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2;
2196     },
2197
2198     /**
2199      * Rebuilds pod row using users_ and apps_ that were previously set or
2200      * updated.
2201      */
2202     rebuildPods: function() {
2203       var emptyPodRow = this.pods.length == 0;
2204
2205       // Clear existing pods.
2206       this.innerHTML = '';
2207       this.focusedPod_ = undefined;
2208       this.activatedPod_ = undefined;
2209       this.lastFocusedPod_ = undefined;
2210
2211       // Switch off animation
2212       Oobe.getInstance().toggleClass('flying-pods', false);
2213
2214       // Populate the pod row.
2215       for (var i = 0; i < this.users_.length; ++i)
2216         this.addUserPod(this.users_[i]);
2217
2218       for (var i = 0, pod; pod = this.pods[i]; ++i)
2219         this.podsWithPendingImages_.push(pod);
2220
2221       // TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
2222       if (this.shouldShowApps_) {
2223         for (var i = 0; i < this.apps_.length; ++i)
2224           this.addUserPod(this.apps_[i]);
2225       }
2226
2227       // Make sure we eventually show the pod row, even if some image is stuck.
2228       setTimeout(function() {
2229         $('pod-row').classList.remove('images-loading');
2230       }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
2231
2232       var isAccountPicker = $('login-header-bar').signinUIState ==
2233           SIGNIN_UI_STATE.ACCOUNT_PICKER;
2234
2235       // Immediately recalculate pods layout only when current UI is account
2236       // picker. Otherwise postpone it.
2237       if (isAccountPicker) {
2238         this.placePods_();
2239         this.maybePreselectPod();
2240
2241         // Without timeout changes in pods positions will be animated even
2242         // though it happened when 'flying-pods' class was disabled.
2243         setTimeout(function() {
2244           Oobe.getInstance().toggleClass('flying-pods', true);
2245         }, 0);
2246       } else {
2247         this.podPlacementPostponed_ = true;
2248
2249         // Update [Cancel] button state.
2250         if ($('login-header-bar').signinUIState ==
2251                 SIGNIN_UI_STATE.GAIA_SIGNIN &&
2252             emptyPodRow &&
2253             this.pods.length > 0) {
2254           login.GaiaSigninScreen.updateCancelButtonState();
2255         }
2256       }
2257     },
2258
2259     /**
2260      * Adds given apps to the pod row.
2261      * @param {array} apps Array of apps.
2262      */
2263     setApps: function(apps) {
2264       this.apps_ = apps;
2265       this.rebuildPods();
2266       chrome.send('kioskAppsLoaded');
2267
2268       // Check whether there's a pending kiosk app error.
2269       window.setTimeout(function() {
2270         chrome.send('checkKioskAppLaunchError');
2271       }, 500);
2272     },
2273
2274     /**
2275      * Sets whether should show app pods.
2276      * @param {boolean} shouldShowApps Whether app pods should be shown.
2277      */
2278     setShouldShowApps: function(shouldShowApps) {
2279       if (this.shouldShowApps_ == shouldShowApps)
2280         return;
2281
2282       this.shouldShowApps_ = shouldShowApps;
2283       this.rebuildPods();
2284     },
2285
2286     /**
2287      * Shows a custom icon on a user pod besides the input field.
2288      * @param {string} username Username of pod to add button
2289      * @param {!{resourceUrl: (string | undefined),
2290      *           data: ({scale1x: string, scale2x: string} | undefined),
2291      *           size: ({width: number, height: number} | undefined),
2292      *           animation: ({resourceWidth: number, frameLength: number} |
2293      *                       undefined),
2294      *           opacity: (number | undefined),
2295      *           tooltip: ({text: string, autoshow: boolean} | undefined)}} icon
2296      *     The icon parameters.
2297      */
2298     showUserPodCustomIcon: function(username, icon) {
2299       var pod = this.getPodWithUsername_(username);
2300       if (pod == null) {
2301         console.error('Unable to show user pod button for ' + username +
2302                       ': user pod not found.');
2303         return;
2304       }
2305
2306       if (icon.resourceUrl) {
2307         pod.customIconElement.setIconAsResourceUrl(icon.resourceUrl);
2308       } else if (icon.data) {
2309         pod.customIconElement.setIconAsImageSet(icon.data);
2310       } else {
2311         return;
2312       }
2313
2314       pod.customIconElement.setSize(icon.size || {width: 0, height: 0});
2315       pod.customIconElement.setAnimation(icon.animation || null);
2316       pod.customIconElement.setOpacity(icon.opacity || 100);
2317       if (icon.hardlockOnClick) {
2318         pod.customIconElement.setInteractive(
2319             this.hardlockUserPod_.bind(this, username));
2320       } else {
2321         pod.customIconElement.setInteractive(null);
2322       }
2323       pod.customIconElement.show();
2324       // This has to be called after |show| in case the tooltip should be shown
2325       // immediatelly.
2326       pod.customIconElement.setTooltip(
2327           icon.tooltip || {text: '', autoshow: false});
2328     },
2329
2330     /**
2331      * Hard-locks user pod for the user. If user pod is hard-locked, it can be
2332      * only unlocked using password, and the authentication type cannot be
2333      * changed.
2334      * @param {!string} username The user's username.
2335      * @private
2336      */
2337     hardlockUserPod_: function(username) {
2338       chrome.send('hardlockPod', [username]);
2339     },
2340
2341     /**
2342      * Hides the custom icon in the user pod added by showUserPodCustomIcon().
2343      * @param {string} username Username of pod to remove button
2344      */
2345     hideUserPodCustomIcon: function(username) {
2346       var pod = this.getPodWithUsername_(username);
2347       if (pod == null) {
2348         console.error('Unable to hide user pod button for ' + username +
2349                       ': user pod not found.');
2350         return;
2351       }
2352
2353       // TODO(tengs): Allow option for a fading transition.
2354       pod.customIconElement.hide();
2355     },
2356
2357     /**
2358      * Sets the authentication type used to authenticate the user.
2359      * @param {string} username Username of selected user
2360      * @param {number} authType Authentication type, must be one of the
2361      *                          values listed in AUTH_TYPE enum.
2362      * @param {string} value The initial value to use for authentication.
2363      */
2364     setAuthType: function(username, authType, value) {
2365       var pod = this.getPodWithUsername_(username);
2366       if (pod == null) {
2367         console.error('Unable to set auth type for ' + username +
2368                       ': user pod not found.');
2369         return;
2370       }
2371       pod.setAuthType(authType, value);
2372     },
2373
2374     /**
2375      * Updates the display name shown on a public session pod.
2376      * @param {string} userID The user ID of the public session
2377      * @param {string} displayName The new display name
2378      */
2379     setPublicSessionDisplayName: function(userID, displayName) {
2380       var pod = this.getPodWithUsername_(userID);
2381       if (pod != null)
2382         pod.setDisplayName(displayName);
2383     },
2384
2385     /**
2386      * Updates the list of locales available for a public session.
2387      * @param {string} userID The user ID of the public session
2388      * @param {!Object} locales The list of available locales
2389      * @param {string} defaultLocale The locale to select by default
2390      * @param {boolean} multipleRecommendedLocales Whether |locales| contains
2391      *     two or more recommended locales
2392      */
2393     setPublicSessionLocales: function(userID,
2394                                       locales,
2395                                       defaultLocale,
2396                                       multipleRecommendedLocales) {
2397       var pod = this.getPodWithUsername_(userID);
2398       if (pod != null) {
2399         pod.populateLanguageSelect(locales,
2400                                    defaultLocale,
2401                                    multipleRecommendedLocales);
2402       }
2403     },
2404
2405     /**
2406      * Updates the list of available keyboard layouts for a public session pod.
2407      * @param {string} userID The user ID of the public session
2408      * @param {string} locale The locale to which this list of keyboard layouts
2409      *     applies
2410      * @param {!Object} list List of available keyboard layouts
2411      */
2412     setPublicSessionKeyboardLayouts: function(userID, locale, list) {
2413       var pod = this.getPodWithUsername_(userID);
2414       if (pod != null)
2415         pod.populateKeyboardSelect(locale, list);
2416     },
2417
2418     /**
2419      * Called when window was resized.
2420      */
2421     onWindowResize: function() {
2422       var layout = this.calculateLayout_();
2423       if (layout.columns != this.columns || layout.rows != this.rows)
2424         this.placePods_();
2425
2426       if (Oobe.getInstance().virtualKeyboardShown)
2427         this.scrollFocusedPodIntoView();
2428     },
2429
2430     /**
2431      * Returns width of podrow having |columns| number of columns.
2432      * @private
2433      */
2434     columnsToWidth_: function(columns) {
2435       var isDesktopUserManager = Oobe.getInstance().displayType ==
2436           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2437       var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2438                                           MARGIN_BY_COLUMNS[columns];
2439       var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2440                                               POD_ROW_PADDING;
2441       return 2 * rowPadding + columns * this.userPodWidth_ +
2442           (columns - 1) * margin;
2443     },
2444
2445     /**
2446      * Returns height of podrow having |rows| number of rows.
2447      * @private
2448      */
2449     rowsToHeight_: function(rows) {
2450       var isDesktopUserManager = Oobe.getInstance().displayType ==
2451           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2452       var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2453                                               POD_ROW_PADDING;
2454       return 2 * rowPadding + rows * this.userPodHeight_;
2455     },
2456
2457     /**
2458      * Calculates number of columns and rows that podrow should have in order to
2459      * hold as much its pods as possible for current screen size. Also it tries
2460      * to choose layout that looks good.
2461      * @return {{columns: number, rows: number}}
2462      */
2463     calculateLayout_: function() {
2464       var preferredColumns = this.pods.length < COLUMNS.length ?
2465           COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
2466       var maxWidth = Oobe.getInstance().clientAreaSize.width;
2467       var columns = preferredColumns;
2468       while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
2469         --columns;
2470       var rows = Math.floor((this.pods.length - 1) / columns) + 1;
2471       if (getComputedStyle(
2472           $('signin-banner'), null).getPropertyValue('display') != 'none') {
2473         rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER);
2474       }
2475       var maxHeigth = Oobe.getInstance().clientAreaSize.height;
2476       while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
2477         --rows;
2478       // One more iteration if it's not enough cells to place all pods.
2479       while (maxWidth >= this.columnsToWidth_(columns + 1) &&
2480              columns * rows < this.pods.length &&
2481              columns < MAX_NUMBER_OF_COLUMNS) {
2482          ++columns;
2483       }
2484       return {columns: columns, rows: rows};
2485     },
2486
2487     /**
2488      * Places pods onto their positions onto pod grid.
2489      * @private
2490      */
2491     placePods_: function() {
2492       var layout = this.calculateLayout_();
2493       var columns = this.columns = layout.columns;
2494       var rows = this.rows = layout.rows;
2495       var maxPodsNumber = columns * rows;
2496       var isDesktopUserManager = Oobe.getInstance().displayType ==
2497           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2498       var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2499                                           MARGIN_BY_COLUMNS[columns];
2500       this.parentNode.setPreferredSize(
2501           this.columnsToWidth_(columns), this.rowsToHeight_(rows));
2502       var height = this.userPodHeight_;
2503       var width = this.userPodWidth_;
2504       this.pods.forEach(function(pod, index) {
2505         if (index >= maxPodsNumber) {
2506            pod.hidden = true;
2507            return;
2508         }
2509         pod.hidden = false;
2510         if (pod.offsetHeight != height) {
2511           console.error('Pod offsetHeight (' + pod.offsetHeight +
2512               ') and POD_HEIGHT (' + height + ') are not equal.');
2513         }
2514         if (pod.offsetWidth != width) {
2515           console.error('Pod offsetWidth (' + pod.offsetWidth +
2516               ') and POD_WIDTH (' + width + ') are not equal.');
2517         }
2518         var column = index % columns;
2519         var row = Math.floor(index / columns);
2520         var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2521                                                 POD_ROW_PADDING;
2522         pod.left = rowPadding + column * (width + margin);
2523
2524         // On desktop, we want the rows to always be equally spaced.
2525         pod.top = isDesktopUserManager ? row * (height + rowPadding) :
2526                                          row * height + rowPadding;
2527       });
2528       Oobe.getInstance().updateScreenSize(this.parentNode);
2529     },
2530
2531     /**
2532      * Number of columns.
2533      * @type {?number}
2534      */
2535     set columns(columns) {
2536       // Cannot use 'columns' here.
2537       this.setAttribute('ncolumns', columns);
2538     },
2539     get columns() {
2540       return parseInt(this.getAttribute('ncolumns'));
2541     },
2542
2543     /**
2544      * Number of rows.
2545      * @type {?number}
2546      */
2547     set rows(rows) {
2548       // Cannot use 'rows' here.
2549       this.setAttribute('nrows', rows);
2550     },
2551     get rows() {
2552       return parseInt(this.getAttribute('nrows'));
2553     },
2554
2555     /**
2556      * Whether the pod is currently focused.
2557      * @param {UserPod} pod Pod to check for focus.
2558      * @return {boolean} Pod focus status.
2559      */
2560     isFocused: function(pod) {
2561       return this.focusedPod_ == pod;
2562     },
2563
2564     /**
2565      * Focuses a given user pod or clear focus when given null.
2566      * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
2567      * @param {boolean=} opt_force If true, forces focus update even when
2568      *     podToFocus is already focused.
2569      */
2570     focusPod: function(podToFocus, opt_force) {
2571       if (this.isFocused(podToFocus) && !opt_force) {
2572         // Calling focusPod w/o podToFocus means reset.
2573         if (!podToFocus)
2574           Oobe.clearErrors();
2575         this.keyboardActivated_ = false;
2576         return;
2577       }
2578
2579       // Make sure there's only one focusPod operation happening at a time.
2580       if (this.insideFocusPod_) {
2581         this.keyboardActivated_ = false;
2582         return;
2583       }
2584       this.insideFocusPod_ = true;
2585
2586       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2587         if (!this.alwaysFocusSinglePod) {
2588           pod.isActionBoxMenuActive = false;
2589         }
2590         if (pod != podToFocus) {
2591           pod.isActionBoxMenuHovered = false;
2592           pod.classList.remove('focused');
2593           // On Desktop, the faded style is not set correctly, so we should
2594           // manually fade out non-focused pods if there is a focused pod.
2595           if (pod.user.isDesktopUser && podToFocus)
2596             pod.classList.add('faded');
2597           else
2598             pod.classList.remove('faded');
2599           pod.reset(false);
2600         }
2601       }
2602
2603       // Clear any error messages for previous pod.
2604       if (!this.isFocused(podToFocus))
2605         Oobe.clearErrors();
2606
2607       var hadFocus = !!this.focusedPod_;
2608       this.focusedPod_ = podToFocus;
2609       if (podToFocus) {
2610         podToFocus.classList.remove('faded');
2611         podToFocus.classList.add('focused');
2612         if (!podToFocus.multiProfilesPolicyApplied)
2613           podToFocus.reset(true);  // Reset and give focus.
2614         else {
2615           podToFocus.userTypeBubbleElement.classList.add('bubble-shown');
2616           podToFocus.focus();
2617         }
2618
2619         // focusPod() automatically loads wallpaper
2620         if (!podToFocus.user.isApp)
2621           chrome.send('focusPod', [podToFocus.user.username]);
2622         this.firstShown_ = false;
2623         this.lastFocusedPod_ = podToFocus;
2624
2625         if (Oobe.getInstance().virtualKeyboardShown)
2626           this.scrollFocusedPodIntoView();
2627       }
2628       this.insideFocusPod_ = false;
2629       this.keyboardActivated_ = false;
2630     },
2631
2632     /**
2633      * Resets wallpaper to the last active user's wallpaper, if any.
2634      */
2635     loadLastWallpaper: function() {
2636       if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp)
2637         chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
2638     },
2639
2640     /**
2641      * Returns the currently activated pod.
2642      * @type {UserPod}
2643      */
2644     get activatedPod() {
2645       return this.activatedPod_;
2646     },
2647
2648     /**
2649      * Sets currently activated pod.
2650      * @param {UserPod} pod Pod to check for focus.
2651      * @param {Event} e Event object.
2652      */
2653     setActivatedPod: function(pod, e) {
2654       if (pod && pod.activate(e))
2655         this.activatedPod_ = pod;
2656     },
2657
2658     /**
2659      * The pod of the signed-in user, if any; null otherwise.
2660      * @type {?UserPod}
2661      */
2662     get lockedPod() {
2663       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2664         if (pod.user.signedIn)
2665           return pod;
2666       }
2667       return null;
2668     },
2669
2670     /**
2671      * The pod that is preselected on user pod row show.
2672      * @type {?UserPod}
2673      */
2674     get preselectedPod() {
2675       var isDesktopUserManager = Oobe.getInstance().displayType ==
2676           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2677       if (isDesktopUserManager) {
2678         // On desktop, don't pre-select a pod if it's the only one.
2679         if (this.pods.length == 1)
2680           return null;
2681
2682         // The desktop User Manager can send the index of a pod that should be
2683         // initially focused in url hash.
2684         var podIndex = parseInt(window.location.hash.substr(1));
2685         if (isNaN(podIndex) || podIndex >= this.pods.length)
2686           return null;
2687         return this.pods[podIndex];
2688       }
2689
2690       var lockedPod = this.lockedPod;
2691       if (lockedPod)
2692         return lockedPod;
2693       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2694         if (!pod.multiProfilesPolicyApplied) {
2695           return pod;
2696         }
2697       }
2698       return this.pods[0];
2699     },
2700
2701     /**
2702      * Resets input UI.
2703      * @param {boolean} takeFocus True to take focus.
2704      */
2705     reset: function(takeFocus) {
2706       this.disabled = false;
2707       if (this.activatedPod_)
2708         this.activatedPod_.reset(takeFocus);
2709     },
2710
2711     /**
2712      * Restores input focus to current selected pod, if there is any.
2713      */
2714     refocusCurrentPod: function() {
2715       if (this.focusedPod_ && !this.focusedPod_.multiProfilesPolicyApplied) {
2716         this.focusedPod_.focusInput();
2717       }
2718     },
2719
2720     /**
2721      * Clears focused pod password field.
2722      */
2723     clearFocusedPod: function() {
2724       if (!this.disabled && this.focusedPod_)
2725         this.focusedPod_.reset(true);
2726     },
2727
2728     /**
2729      * Shows signin UI.
2730      * @param {string} email Email for signin UI.
2731      */
2732     showSigninUI: function(email) {
2733       // Clear any error messages that might still be around.
2734       Oobe.clearErrors();
2735       this.disabled = true;
2736       this.lastFocusedPod_ = this.getPodWithUsername_(email);
2737       Oobe.showSigninUI(email);
2738     },
2739
2740     /**
2741      * Updates current image of a user.
2742      * @param {string} username User for which to update the image.
2743      */
2744     updateUserImage: function(username) {
2745       var pod = this.getPodWithUsername_(username);
2746       if (pod)
2747         pod.updateUserImage();
2748     },
2749
2750     /**
2751      * Handler of click event.
2752      * @param {Event} e Click Event object.
2753      * @private
2754      */
2755     handleClick_: function(e) {
2756       if (this.disabled)
2757         return;
2758
2759       // Clear all menus if the click is outside pod menu and its
2760       // button area.
2761       if (!findAncestorByClass(e.target, 'action-box-menu') &&
2762           !findAncestorByClass(e.target, 'action-box-area')) {
2763         for (var i = 0, pod; pod = this.pods[i]; ++i)
2764           pod.isActionBoxMenuActive = false;
2765       }
2766
2767       // Clears focus if not clicked on a pod and if there's more than one pod.
2768       var pod = findAncestorByClass(e.target, 'pod');
2769       if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) {
2770         this.focusPod();
2771       }
2772
2773       if (pod)
2774         pod.isActionBoxMenuHovered = true;
2775
2776       // Return focus back to single pod.
2777       if (this.alwaysFocusSinglePod && !pod) {
2778         this.focusPod(this.focusedPod_, true /* force */);
2779         this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
2780         this.focusedPod_.isActionBoxMenuHovered = false;
2781       }
2782     },
2783
2784     /**
2785      * Handler of mouse move event.
2786      * @param {Event} e Click Event object.
2787      * @private
2788      */
2789     handleMouseMove_: function(e) {
2790       if (this.disabled)
2791         return;
2792       if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
2793         return;
2794
2795       // Defocus (thus hide) action box, if it is focused on a user pod
2796       // and the pointer is not hovering over it.
2797       var pod = findAncestorByClass(e.target, 'pod');
2798       if (document.activeElement &&
2799           document.activeElement.parentNode != pod &&
2800           document.activeElement.classList.contains('action-box-area')) {
2801         document.activeElement.parentNode.focus();
2802       }
2803
2804       if (pod)
2805         pod.isActionBoxMenuHovered = true;
2806
2807       // Hide action boxes on other user pods.
2808       for (var i = 0, p; p = this.pods[i]; ++i)
2809         if (p != pod && !p.isActionBoxMenuActive)
2810           p.isActionBoxMenuHovered = false;
2811     },
2812
2813     /**
2814      * Handles focus event.
2815      * @param {Event} e Focus Event object.
2816      * @private
2817      */
2818     handleFocus_: function(e) {
2819       if (this.disabled)
2820         return;
2821       if (e.target.parentNode == this) {
2822         // Focus on a pod
2823         if (e.target.classList.contains('focused')) {
2824           if (!e.target.multiProfilesPolicyApplied)
2825             e.target.focusInput();
2826           else
2827             e.target.userTypeBubbleElement.classList.add('bubble-shown');
2828         } else
2829           this.focusPod(e.target);
2830         return;
2831       }
2832
2833       var pod = findAncestorByClass(e.target, 'pod');
2834       if (pod && pod.parentNode == this) {
2835         // Focus on a control of a pod but not on the action area button.
2836         if (!pod.classList.contains('focused') &&
2837             !e.target.classList.contains('action-box-button')) {
2838           this.focusPod(pod);
2839           pod.userTypeBubbleElement.classList.remove('bubble-shown');
2840           e.target.focus();
2841         }
2842         return;
2843       }
2844
2845       // Clears pod focus when we reach here. It means new focus is neither
2846       // on a pod nor on a button/input for a pod.
2847       // Do not "defocus" user pod when it is a single pod.
2848       // That means that 'focused' class will not be removed and
2849       // input field/button will always be visible.
2850       if (!this.alwaysFocusSinglePod)
2851         this.focusPod();
2852       else {
2853         // Hide user-type-bubble in case this is one pod and we lost focus of
2854         // it.
2855         this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
2856       }
2857     },
2858
2859     /**
2860      * Handler of keydown event.
2861      * @param {Event} e KeyDown Event object.
2862      */
2863     handleKeyDown: function(e) {
2864       if (this.disabled)
2865         return;
2866       var editing = e.target.tagName == 'INPUT' && e.target.value;
2867       switch (e.keyIdentifier) {
2868         case 'Left':
2869           if (!editing) {
2870             this.keyboardActivated_ = true;
2871             if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
2872               this.focusPod(this.focusedPod_.previousElementSibling);
2873             else
2874               this.focusPod(this.lastElementChild);
2875
2876             e.stopPropagation();
2877           }
2878           break;
2879         case 'Right':
2880           if (!editing) {
2881             this.keyboardActivated_ = true;
2882             if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
2883               this.focusPod(this.focusedPod_.nextElementSibling);
2884             else
2885               this.focusPod(this.firstElementChild);
2886
2887             e.stopPropagation();
2888           }
2889           break;
2890         case 'Enter':
2891           if (this.focusedPod_) {
2892             var targetTag = e.target.tagName;
2893             if (e.target == this.focusedPod_.passwordElement ||
2894                 (targetTag != 'INPUT' &&
2895                  targetTag != 'BUTTON' &&
2896                  targetTag != 'A')) {
2897               this.setActivatedPod(this.focusedPod_, e);
2898               e.stopPropagation();
2899             }
2900           }
2901           break;
2902         case 'U+001B':  // Esc
2903           if (!this.alwaysFocusSinglePod)
2904             this.focusPod();
2905           break;
2906       }
2907     },
2908
2909     /**
2910      * Called right after the pod row is shown.
2911      */
2912     handleAfterShow: function() {
2913       // Without timeout changes in pods positions will be animated even though
2914       // it happened when 'flying-pods' class was disabled.
2915       setTimeout(function() {
2916         Oobe.getInstance().toggleClass('flying-pods', true);
2917       }, 0);
2918       // Force input focus for user pod on show and once transition ends.
2919       if (this.focusedPod_) {
2920         var focusedPod = this.focusedPod_;
2921         var screen = this.parentNode;
2922         var self = this;
2923         focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
2924           focusedPod.removeEventListener('webkitTransitionEnd', f);
2925           focusedPod.reset(true);
2926           // Notify screen that it is ready.
2927           screen.onShow();
2928         });
2929         // Guard timer for 1 second -- it would conver all possible animations.
2930         ensureTransitionEndEvent(focusedPod, 1000);
2931       }
2932     },
2933
2934     /**
2935      * Called right before the pod row is shown.
2936      */
2937     handleBeforeShow: function() {
2938       Oobe.getInstance().toggleClass('flying-pods', false);
2939       for (var event in this.listeners_) {
2940         this.ownerDocument.addEventListener(
2941             event, this.listeners_[event][0], this.listeners_[event][1]);
2942       }
2943       $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
2944
2945       if (this.podPlacementPostponed_) {
2946         this.podPlacementPostponed_ = false;
2947         this.placePods_();
2948         this.maybePreselectPod();
2949       }
2950     },
2951
2952     /**
2953      * Called when the element is hidden.
2954      */
2955     handleHide: function() {
2956       for (var event in this.listeners_) {
2957         this.ownerDocument.removeEventListener(
2958             event, this.listeners_[event][0], this.listeners_[event][1]);
2959       }
2960       $('login-header-bar').buttonsTabIndex = 0;
2961     },
2962
2963     /**
2964      * Called when a pod's user image finishes loading.
2965      */
2966     handlePodImageLoad: function(pod) {
2967       var index = this.podsWithPendingImages_.indexOf(pod);
2968       if (index == -1) {
2969         return;
2970       }
2971
2972       this.podsWithPendingImages_.splice(index, 1);
2973       if (this.podsWithPendingImages_.length == 0) {
2974         this.classList.remove('images-loading');
2975       }
2976     },
2977
2978     /**
2979      * Preselects pod, if needed.
2980      */
2981      maybePreselectPod: function() {
2982        var pod = this.preselectedPod;
2983        this.focusPod(pod);
2984
2985        // Hide user-type-bubble in case all user pods are disabled and we focus
2986        // first pod.
2987        if (pod && pod.multiProfilesPolicyApplied) {
2988          pod.userTypeBubbleElement.classList.remove('bubble-shown');
2989        }
2990      }
2991   };
2992
2993   return {
2994     PodRow: PodRow
2995   };
2996 });