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