Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / ui / login / account_picker / user_pod_row.js
bloba8ab31671f9b8187395807bf7674e0572c29d9e4
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.reauthWarningElement.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 reauth name hint element.
837      * @type {!HTMLDivElement}
838      */
839     get reauthNameHintElement() {
840       return this.querySelector('.reauth-name-hint');
841     },
843     /**
844      * Gets the container holding the password field.
845      * @type {!HTMLInputElement}
846      */
847     get passwordEntryContainerElement() {
848       return this.querySelector('.password-entry-container');
849     },
851     /**
852      * Gets password field.
853      * @type {!HTMLInputElement}
854      */
855     get passwordElement() {
856       return this.querySelector('.password');
857     },
859     /**
860      * Gets the password label, which is used to show a message where the
861      * password field is normally.
862      * @type {!HTMLInputElement}
863      */
864     get passwordLabelElement() {
865       return this.querySelector('.password-label');
866     },
868     /**
869      * Gets user online sign in hint element.
870      * @type {!HTMLDivElement}
871      */
872     get reauthWarningElement() {
873       return this.querySelector('.reauth-hint-container');
874     },
876     /**
877      * Gets the container holding the launch app button.
878      * @type {!HTMLButtonElement}
879      */
880     get launchAppButtonContainerElement() {
881       return this.querySelector('.launch-app-button-container');
882     },
884     /**
885      * Gets launch app button.
886      * @type {!HTMLButtonElement}
887      */
888     get launchAppButtonElement() {
889       return this.querySelector('.launch-app-button');
890     },
892     /**
893      * Gets action box area.
894      * @type {!HTMLInputElement}
895      */
896     get actionBoxAreaElement() {
897       return this.querySelector('.action-box-area');
898     },
900     /**
901      * Gets user type icon area.
902      * @type {!HTMLDivElement}
903      */
904     get userTypeIconAreaElement() {
905       return this.querySelector('.user-type-icon-area');
906     },
908     /**
909      * Gets user type bubble like multi-profiles policy restriction message.
910      * @type {!HTMLDivElement}
911      */
912     get userTypeBubbleElement() {
913       return this.querySelector('.user-type-bubble');
914     },
916     /**
917      * Gets action box menu.
918      * @type {!HTMLInputElement}
919      */
920     get actionBoxMenu() {
921       return this.querySelector('.action-box-menu');
922     },
924     /**
925      * Gets action box menu title, user name item.
926      * @type {!HTMLInputElement}
927      */
928     get actionBoxMenuTitleNameElement() {
929       return this.querySelector('.action-box-menu-title-name');
930     },
932     /**
933      * Gets action box menu title, user email item.
934      * @type {!HTMLInputElement}
935      */
936     get actionBoxMenuTitleEmailElement() {
937       return this.querySelector('.action-box-menu-title-email');
938     },
940     /**
941      * Gets action box menu, remove user command item.
942      * @type {!HTMLInputElement}
943      */
944     get actionBoxMenuCommandElement() {
945       return this.querySelector('.action-box-menu-remove-command');
946     },
948     /**
949      * Gets action box menu, remove user command item div.
950      * @type {!HTMLInputElement}
951      */
952     get actionBoxMenuRemoveElement() {
953       return this.querySelector('.action-box-menu-remove');
954     },
956     /**
957      * Gets action box menu, remove user warning text div.
958      * @type {!HTMLInputElement}
959      */
960     get actionBoxRemoveUserWarningTextElement() {
961       return this.querySelector('.action-box-remove-user-warning-text');
962     },
964     /**
965      * Gets action box menu, remove legacy supervised user warning text div.
966      * @type {!HTMLInputElement}
967      */
968     get actionBoxRemoveLegacySupervisedUserWarningTextElement() {
969       return this.querySelector(
970           '.action-box-remove-legacy-supervised-user-warning-text');
971     },
973     /**
974      * Gets action box menu, remove user command item div.
975      * @type {!HTMLInputElement}
976      */
977     get actionBoxRemoveUserWarningElement() {
978       return this.querySelector('.action-box-remove-user-warning');
979     },
981     /**
982      * Gets action box menu, remove user command item div.
983      * @type {!HTMLInputElement}
984      */
985     get actionBoxRemoveUserWarningButtonElement() {
986       return this.querySelector('.remove-warning-button');
987     },
989     /**
990      * Gets the custom icon. This icon is normally hidden, but can be shown
991      * using the chrome.screenlockPrivate API.
992      * @type {!HTMLDivElement}
993      */
994     get customIconElement() {
995       return this.querySelector('.custom-icon-container');
996     },
998     /**
999      * Updates the user pod element.
1000      */
1001     update: function() {
1002       this.imageElement.src = 'chrome://userimage/' + this.user.username +
1003           '?id=' + UserPod.userImageSalt_[this.user.username];
1005       this.nameElement.textContent = this.user_.displayName;
1006       this.reauthNameHintElement.textContent = this.user_.displayName;
1007       this.classList.toggle('signed-in', this.user_.signedIn);
1009       if (this.isAuthTypeUserClick)
1010         this.passwordLabelElement.textContent = this.authValue;
1012       this.updateActionBoxArea();
1014       this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
1015         'passwordFieldAccessibleName', this.user_.emailAddress));
1017       this.customizeUserPodPerUserType();
1018     },
1020     updateActionBoxArea: function() {
1021       if (this.user_.publicAccount || this.user_.isApp) {
1022         this.actionBoxAreaElement.hidden = true;
1023         return;
1024       }
1026       this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
1028       this.actionBoxAreaElement.setAttribute(
1029           'aria-label', loadTimeData.getStringF(
1030               'podMenuButtonAccessibleName', this.user_.emailAddress));
1031       this.actionBoxMenuRemoveElement.setAttribute(
1032           'aria-label', loadTimeData.getString(
1033                'podMenuRemoveItemAccessibleName'));
1034       this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
1035           loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
1036           this.user_.displayName;
1037       this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
1039       this.actionBoxMenuTitleEmailElement.hidden =
1040           this.user_.legacySupervisedUser;
1042       this.actionBoxMenuCommandElement.textContent =
1043           loadTimeData.getString('removeUser');
1044     },
1046     customizeUserPodPerUserType: function() {
1047       if (this.user_.childUser && !this.user_.isDesktopUser) {
1048         this.setUserPodIconType('child');
1049       } else if (this.user_.legacySupervisedUser && !this.user_.isDesktopUser) {
1050         this.setUserPodIconType('legacySupervised');
1051       } else if (this.multiProfilesPolicyApplied) {
1052         // Mark user pod as not focusable which in addition to the grayed out
1053         // filter makes it look in disabled state.
1054         this.classList.add('multiprofiles-policy-applied');
1055         this.setUserPodIconType('policy');
1057         if (this.user.multiProfilesPolicy == 'primary-only')
1058           this.querySelector('.mp-policy-primary-only-msg').hidden = false;
1059         else if (this.user.multiProfilesPolicy == 'owner-primary-only')
1060           this.querySelector('.mp-owner-primary-only-msg').hidden = false;
1061         else
1062           this.querySelector('.mp-policy-not-allowed-msg').hidden = false;
1063       } else if (this.user_.isApp) {
1064         this.setUserPodIconType('app');
1065       }
1066     },
1068     setUserPodIconType: function(userTypeClass) {
1069       this.userTypeIconAreaElement.classList.add(userTypeClass);
1070       this.userTypeIconAreaElement.hidden = false;
1071     },
1073     /**
1074      * The user that this pod represents.
1075      * @type {!Object}
1076      */
1077     user_: undefined,
1078     get user() {
1079       return this.user_;
1080     },
1081     set user(userDict) {
1082       this.user_ = userDict;
1083       this.update();
1084     },
1086     /**
1087      * Returns true if multi-profiles sign in is currently active and this
1088      * user pod is restricted per policy.
1089      * @type {boolean}
1090      */
1091     get multiProfilesPolicyApplied() {
1092       var isMultiProfilesUI =
1093         (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING);
1094       return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed;
1095     },
1097     /**
1098      * Gets main input element.
1099      * @type {(HTMLButtonElement|HTMLInputElement)}
1100      */
1101     get mainInput() {
1102       if (this.isAuthTypePassword) {
1103         return this.passwordElement;
1104       } else if (this.isAuthTypeOnlineSignIn) {
1105         return this;
1106       } else if (this.isAuthTypeUserClick) {
1107         return this.passwordLabelElement;
1108       }
1109     },
1111     /**
1112      * Whether action box button is in active state.
1113      * @type {boolean}
1114      */
1115     get isActionBoxMenuActive() {
1116       return this.actionBoxAreaElement.classList.contains('active');
1117     },
1118     set isActionBoxMenuActive(active) {
1119       if (active == this.isActionBoxMenuActive)
1120         return;
1122       if (active) {
1123         this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
1124         this.actionBoxRemoveUserWarningElement.hidden = true;
1126         // Clear focus first if another pod is focused.
1127         if (!this.parentNode.isFocused(this)) {
1128           this.parentNode.focusPod(undefined, true);
1129           this.actionBoxAreaElement.focus();
1130         }
1132         // Hide user-type-bubble.
1133         this.userTypeBubbleElement.classList.remove('bubble-shown');
1135         this.actionBoxAreaElement.classList.add('active');
1137         // If the user pod is on either edge of the screen, then the menu
1138         // could be displayed partially ofscreen.
1139         this.actionBoxMenu.classList.remove('left-edge-offset');
1140         this.actionBoxMenu.classList.remove('right-edge-offset');
1142         var offsetLeft =
1143             cr.ui.login.DisplayManager.getOffset(this.actionBoxMenu).left;
1144         var menuWidth = this.actionBoxMenu.offsetWidth;
1145         if (offsetLeft < 0)
1146           this.actionBoxMenu.classList.add('left-edge-offset');
1147         else if (offsetLeft + menuWidth > window.innerWidth)
1148           this.actionBoxMenu.classList.add('right-edge-offset');
1149       } else {
1150         this.actionBoxAreaElement.classList.remove('active');
1151         this.actionBoxAreaElement.classList.remove('menu-moved-up');
1152         this.actionBoxMenu.classList.remove('menu-moved-up');
1153       }
1154     },
1156     /**
1157      * Whether action box button is in hovered state.
1158      * @type {boolean}
1159      */
1160     get isActionBoxMenuHovered() {
1161       return this.actionBoxAreaElement.classList.contains('hovered');
1162     },
1163     set isActionBoxMenuHovered(hovered) {
1164       if (hovered == this.isActionBoxMenuHovered)
1165         return;
1167       if (hovered) {
1168         this.actionBoxAreaElement.classList.add('hovered');
1169         this.classList.add('hovered');
1170       } else {
1171         if (this.multiProfilesPolicyApplied)
1172           this.userTypeBubbleElement.classList.remove('bubble-shown');
1173         this.actionBoxAreaElement.classList.remove('hovered');
1174         this.classList.remove('hovered');
1175       }
1176     },
1178     /**
1179      * Set the authentication type for the pod.
1180      * @param {number} An auth type value defined in the AUTH_TYPE enum.
1181      * @param {string} authValue The initial value used for the auth type.
1182      */
1183     setAuthType: function(authType, authValue) {
1184       this.authType_ = authType;
1185       this.authValue_ = authValue;
1186       this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]);
1187       this.update();
1188       this.reset(this.parentNode.isFocused(this));
1189     },
1191     /**
1192      * The auth type of the user pod. This value is one of the enum
1193      * values in AUTH_TYPE.
1194      * @type {number}
1195      */
1196     get authType() {
1197       return this.authType_;
1198     },
1200     /**
1201      * The initial value used for the pod's authentication type.
1202      * eg. a prepopulated password input when using password authentication.
1203      */
1204     get authValue() {
1205       return this.authValue_;
1206     },
1208     /**
1209      * True if the the user pod uses a password to authenticate.
1210      * @type {bool}
1211      */
1212     get isAuthTypePassword() {
1213       return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD ||
1214              this.authType_ == AUTH_TYPE.FORCE_OFFLINE_PASSWORD;
1215     },
1217     /**
1218      * True if the the user pod uses a user click to authenticate.
1219      * @type {bool}
1220      */
1221     get isAuthTypeUserClick() {
1222       return this.authType_ == AUTH_TYPE.USER_CLICK;
1223     },
1225     /**
1226      * True if the the user pod uses a online sign in to authenticate.
1227      * @type {bool}
1228      */
1229     get isAuthTypeOnlineSignIn() {
1230       return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN;
1231     },
1233     /**
1234      * Updates the image element of the user.
1235      */
1236     updateUserImage: function() {
1237       UserPod.userImageSalt_[this.user.username] = new Date().getTime();
1238       this.update();
1239     },
1241     /**
1242      * Focuses on input element.
1243      */
1244     focusInput: function() {
1245       // Move tabIndex from the whole pod to the main input.
1246       // Note: the |mainInput| can be the pod itself.
1247       this.tabIndex = -1;
1248       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1249       this.mainInput.focus();
1250     },
1252     /**
1253      * Activates the pod.
1254      * @param {Event} e Event object.
1255      * @return {boolean} True if activated successfully.
1256      */
1257     activate: function(e) {
1258       if (this.isAuthTypeOnlineSignIn) {
1259         this.showSigninUI();
1260       } else if (this.isAuthTypeUserClick) {
1261         Oobe.disableSigninUI();
1262         this.classList.toggle('signing-in', true);
1263         chrome.send('attemptUnlock', [this.user.username]);
1264       } else if (this.isAuthTypePassword) {
1265         if (!this.passwordElement.value)
1266           return false;
1267         Oobe.disableSigninUI();
1268         chrome.send('authenticateUser',
1269                     [this.user.username, this.passwordElement.value]);
1270       } else {
1271         console.error('Activating user pod with invalid authentication type: ' +
1272             this.authType);
1273       }
1275       return true;
1276     },
1278     showSupervisedUserSigninWarning: function() {
1279       // Legacy supervised user token has been invalidated.
1280       // Make sure that pod is focused i.e. "Sign in" button is seen.
1281       this.parentNode.focusPod(this);
1283       var error = document.createElement('div');
1284       var messageDiv = document.createElement('div');
1285       messageDiv.className = 'error-message-bubble';
1286       messageDiv.textContent =
1287           loadTimeData.getString('supervisedUserExpiredTokenWarning');
1288       error.appendChild(messageDiv);
1290       $('bubble').showContentForElement(
1291           this.reauthWarningElement,
1292           cr.ui.Bubble.Attachment.TOP,
1293           error,
1294           this.reauthWarningElement.offsetWidth / 2,
1295           4);
1296       // Move warning bubble up if it overlaps the shelf.
1297       var maxHeight =
1298           cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping($('bubble'));
1299       if (maxHeight < $('bubble').offsetHeight) {
1300         $('bubble').showContentForElement(
1301             this.reauthWarningElement,
1302             cr.ui.Bubble.Attachment.BOTTOM,
1303             error,
1304             this.reauthWarningElement.offsetWidth / 2,
1305             4);
1306       }
1307     },
1309     /**
1310      * Shows signin UI for this user.
1311      */
1312     showSigninUI: function() {
1313       if (this.user.legacySupervisedUser && !this.user.isDesktopUser) {
1314         this.showSupervisedUserSigninWarning();
1315       } else {
1316         // Special case for multi-profiles sign in. We show users even if they
1317         // are not allowed per policy. Restrict those users from starting GAIA.
1318         if (this.multiProfilesPolicyApplied)
1319           return;
1321         this.parentNode.showSigninUI(this.user.emailAddress);
1322       }
1323     },
1325     /**
1326      * Resets the input field and updates the tab order of pod controls.
1327      * @param {boolean} takeFocus If true, input field takes focus.
1328      */
1329     reset: function(takeFocus) {
1330       this.passwordElement.value = '';
1331       this.classList.toggle('signing-in', false);
1332       if (takeFocus) {
1333         if (!this.multiProfilesPolicyApplied)
1334           this.focusInput();  // This will set a custom tab order.
1335       }
1336       else
1337         this.resetTabOrder();
1338     },
1340     /**
1341      * Removes a user using the correct identifier based on user type.
1342      * @param {Object} user User to be removed.
1343      */
1344     removeUser: function(user) {
1345       chrome.send('removeUser',
1346                   [user.isDesktopUser ? user.profilePath : user.username]);
1347     },
1349     /**
1350      * Handles a click event on action area button.
1351      * @param {Event} e Click event.
1352      */
1353     handleActionAreaButtonClick_: function(e) {
1354       if (this.parentNode.disabled)
1355         return;
1356       this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
1357       e.stopPropagation();
1358     },
1360     /**
1361      * Handles a keydown event on action area button.
1362      * @param {Event} e KeyDown event.
1363      */
1364     handleActionAreaButtonKeyDown_: function(e) {
1365       if (this.disabled)
1366         return;
1367       switch (e.keyIdentifier) {
1368         case 'Enter':
1369         case 'U+0020':  // Space
1370           if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
1371             this.isActionBoxMenuActive = true;
1372           e.stopPropagation();
1373           break;
1374         case 'Up':
1375         case 'Down':
1376           if (this.isActionBoxMenuActive) {
1377             this.actionBoxMenuRemoveElement.tabIndex =
1378                 UserPodTabOrder.PAD_MENU_ITEM;
1379             this.actionBoxMenuRemoveElement.focus();
1380           }
1381           e.stopPropagation();
1382           break;
1383         case 'U+001B':  // Esc
1384           this.isActionBoxMenuActive = false;
1385           e.stopPropagation();
1386           break;
1387         case 'U+0009':  // Tab
1388           if (!this.parentNode.alwaysFocusSinglePod)
1389             this.parentNode.focusPod();
1390         default:
1391           this.isActionBoxMenuActive = false;
1392           break;
1393       }
1394     },
1396     /**
1397      * Handles a click event on remove user command.
1398      * @param {Event} e Click event.
1399      */
1400     handleRemoveCommandClick_: function(e) {
1401       if (this.user.legacySupervisedUser || this.user.isDesktopUser) {
1402         this.showRemoveWarning_();
1403         return;
1404       }
1405       if (this.isActionBoxMenuActive)
1406         chrome.send('removeUser', [this.user.username]);
1407     },
1409     /**
1410      * Shows remove user warning. Used for legacy supervised users on CrOS, and
1411      * for all users on desktop.
1412      */
1413     showRemoveWarning_: function() {
1414       this.actionBoxMenuRemoveElement.hidden = true;
1415       this.actionBoxRemoveUserWarningElement.hidden = false;
1416       this.actionBoxRemoveUserWarningButtonElement.focus();
1418       // Move up the menu if it overlaps shelf.
1419       var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(
1420           this.actionBoxMenu);
1421       var actualHeight = parseInt(
1422           window.getComputedStyle(this.actionBoxMenu).height);
1423       if (maxHeight < actualHeight) {
1424         this.actionBoxMenu.classList.add('menu-moved-up');
1425         this.actionBoxAreaElement.classList.add('menu-moved-up');
1426       }
1427       chrome.send('logRemoveUserWarningShown');
1428     },
1430     /**
1431      * Handles a click event on remove user confirmation button.
1432      * @param {Event} e Click event.
1433      */
1434     handleRemoveUserConfirmationClick_: function(e) {
1435       if (this.isActionBoxMenuActive) {
1436         this.isActionBoxMenuActive = false;
1437         this.removeUser(this.user);
1438         e.stopPropagation();
1439       }
1440     },
1442     /**
1443      * Handles a keydown event on remove user confirmation button.
1444      * @param {Event} e KeyDown event.
1445      */
1446     handleRemoveUserConfirmationKeyDown_: function(e) {
1447       if (!this.isActionBoxMenuActive)
1448         return;
1450       // Only handle pressing 'Enter' or 'Space', and let all other events
1451       // bubble to the action box menu.
1452       if (e.keyIdentifier == 'Enter' || e.keyIdentifier == 'U+0020') {
1453         this.isActionBoxMenuActive = false;
1454         this.removeUser(this.user);
1455         e.stopPropagation();
1456         // Prevent default so that we don't trigger a 'click' event.
1457         e.preventDefault();
1458       }
1459     },
1461     /**
1462      * Handles a keydown event on remove command.
1463      * @param {Event} e KeyDown event.
1464      */
1465     handleRemoveCommandKeyDown_: function(e) {
1466       if (this.disabled)
1467         return;
1468       switch (e.keyIdentifier) {
1469         case 'Enter':
1470           if (this.user.legacySupervisedUser || this.user.isDesktopUser) {
1471             // Prevent default so that we don't trigger a 'click' event on the
1472             // remove button that will be focused.
1473             e.preventDefault();
1474             this.showRemoveWarning_();
1475           } else {
1476             this.removeUser(this.user);
1477           }
1478           e.stopPropagation();
1479           break;
1480         case 'Up':
1481         case 'Down':
1482           e.stopPropagation();
1483           break;
1484         case 'U+001B':  // Esc
1485           this.actionBoxAreaElement.focus();
1486           this.isActionBoxMenuActive = false;
1487           e.stopPropagation();
1488           break;
1489         default:
1490           this.actionBoxAreaElement.focus();
1491           this.isActionBoxMenuActive = false;
1492           break;
1493       }
1494     },
1496     /**
1497      * Handles a blur event on remove command.
1498      * @param {Event} e Blur event.
1499      */
1500     handleRemoveCommandBlur_: function(e) {
1501       if (this.disabled)
1502         return;
1503       this.actionBoxMenuRemoveElement.tabIndex = -1;
1504     },
1506     /**
1507      * Handles mouse down event. It sets whether the user click auth will be
1508      * allowed on the next mouse click event. The auth is allowed iff the pod
1509      * was focused on the mouse down event starting the click.
1510      * @param {Event} e The mouse down event.
1511      */
1512     handlePodMouseDown_: function(e) {
1513       this.userClickAuthAllowed_ = this.parentNode.isFocused(this);
1514     },
1516     /**
1517      * Handles click event on a user pod.
1518      * @param {Event} e Click event.
1519      */
1520     handleClickOnPod_: function(e) {
1521       if (this.parentNode.disabled)
1522         return;
1524       if (!this.isActionBoxMenuActive) {
1525         if (this.isAuthTypeOnlineSignIn) {
1526           this.showSigninUI();
1527         } else if (this.isAuthTypeUserClick && this.userClickAuthAllowed_) {
1528           // Note that this.userClickAuthAllowed_ is set in mouse down event
1529           // handler.
1530           this.parentNode.setActivatedPod(this);
1531         }
1533         if (this.multiProfilesPolicyApplied)
1534           this.userTypeBubbleElement.classList.add('bubble-shown');
1536         // Prevent default so that we don't trigger 'focus' event.
1537         e.preventDefault();
1538       }
1539     },
1541     /**
1542      * Handles keydown event for a user pod.
1543      * @param {Event} e Key event.
1544      */
1545     handlePodKeyDown_: function(e) {
1546       if (!this.isAuthTypeUserClick || this.disabled)
1547         return;
1548       switch (e.keyIdentifier) {
1549         case 'Enter':
1550         case 'U+0020':  // Space
1551           if (this.parentNode.isFocused(this))
1552             this.parentNode.setActivatedPod(this);
1553           break;
1554       }
1555     }
1556   };
1558   /**
1559    * Creates a public account user pod.
1560    * @constructor
1561    * @extends {UserPod}
1562    */
1563   var PublicAccountUserPod = cr.ui.define(function() {
1564     var node = UserPod();
1566     var extras = $('public-account-user-pod-extras-template').children;
1567     for (var i = 0; i < extras.length; ++i) {
1568       var el = extras[i].cloneNode(true);
1569       node.appendChild(el);
1570     }
1572     return node;
1573   });
1575   PublicAccountUserPod.prototype = {
1576     __proto__: UserPod.prototype,
1578     /**
1579      * "Enter" button in expanded side pane.
1580      * @type {!HTMLButtonElement}
1581      */
1582     get enterButtonElement() {
1583       return this.querySelector('.enter-button');
1584     },
1586     /**
1587      * Boolean flag of whether the pod is showing the side pane. The flag
1588      * controls whether 'expanded' class is added to the pod's class list and
1589      * resets tab order because main input element changes when the 'expanded'
1590      * state changes.
1591      * @type {boolean}
1592      */
1593     get expanded() {
1594       return this.classList.contains('expanded');
1595     },
1597     set expanded(expanded) {
1598       if (this.expanded == expanded)
1599         return;
1601       this.resetTabOrder();
1602       this.classList.toggle('expanded', expanded);
1603       if (expanded) {
1604         // Show the advanced expanded pod directly if there are at least two
1605         // recommended locales. This will be the case in multilingual
1606         // environments where users are likely to want to choose among locales.
1607         if (this.querySelector('.language-select').multipleRecommendedLocales)
1608           this.classList.add('advanced');
1609         this.usualLeft = this.left;
1610         this.makeSpaceForExpandedPod_();
1611       } else if (typeof(this.usualLeft) != 'undefined') {
1612         this.left = this.usualLeft;
1613       }
1615       var self = this;
1616       this.classList.add('animating');
1617       this.addEventListener('webkitTransitionEnd', function f(e) {
1618         self.removeEventListener('webkitTransitionEnd', f);
1619         self.classList.remove('animating');
1621         // Accessibility focus indicator does not move with the focused
1622         // element. Sends a 'focus' event on the currently focused element
1623         // so that accessibility focus indicator updates its location.
1624         if (document.activeElement)
1625           document.activeElement.dispatchEvent(new Event('focus'));
1626       });
1627       // Guard timer set to animation duration + 20ms.
1628       ensureTransitionEndEvent(this, 200);
1629     },
1631     get advanced() {
1632       return this.classList.contains('advanced');
1633     },
1635     /** @override */
1636     get mainInput() {
1637       if (this.expanded)
1638         return this.enterButtonElement;
1639       else
1640         return this.nameElement;
1641     },
1643     /** @override */
1644     decorate: function() {
1645       UserPod.prototype.decorate.call(this);
1647       this.classList.add('public-account');
1649       this.nameElement.addEventListener('keydown', (function(e) {
1650         if (e.keyIdentifier == 'Enter') {
1651           this.parentNode.setActivatedPod(this, e);
1652           // Stop this keydown event from bubbling up to PodRow handler.
1653           e.stopPropagation();
1654           // Prevent default so that we don't trigger a 'click' event on the
1655           // newly focused "Enter" button.
1656           e.preventDefault();
1657         }
1658       }).bind(this));
1660       var learnMore = this.querySelector('.learn-more');
1661       learnMore.addEventListener('mousedown', stopEventPropagation);
1662       learnMore.addEventListener('click', this.handleLearnMoreEvent);
1663       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1665       learnMore = this.querySelector('.expanded-pane-learn-more');
1666       learnMore.addEventListener('click', this.handleLearnMoreEvent);
1667       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1669       var languageSelect = this.querySelector('.language-select');
1670       languageSelect.tabIndex = UserPodTabOrder.POD_INPUT;
1671       languageSelect.manuallyChanged = false;
1672       languageSelect.addEventListener(
1673           'change',
1674           function() {
1675             languageSelect.manuallyChanged = true;
1676             this.getPublicSessionKeyboardLayouts_();
1677           }.bind(this));
1679       var keyboardSelect = this.querySelector('.keyboard-select');
1680       keyboardSelect.tabIndex = UserPodTabOrder.POD_INPUT;
1681       keyboardSelect.loadedLocale = null;
1683       var languageAndInput = this.querySelector('.language-and-input');
1684       languageAndInput.tabIndex = UserPodTabOrder.POD_INPUT;
1685       languageAndInput.addEventListener('click',
1686                                         this.transitionToAdvanced_.bind(this));
1688       this.enterButtonElement.addEventListener('click', (function(e) {
1689         this.enterButtonElement.disabled = true;
1690         var locale = this.querySelector('.language-select').value;
1691         var keyboardSelect = this.querySelector('.keyboard-select');
1692         // The contents of |keyboardSelect| is updated asynchronously. If its
1693         // locale does not match |locale|, it has not updated yet and the
1694         // currently selected keyboard layout may not be applicable to |locale|.
1695         // Do not return any keyboard layout in this case and let the backend
1696         // choose a suitable layout.
1697         var keyboardLayout =
1698             keyboardSelect.loadedLocale == locale ? keyboardSelect.value : '';
1699         chrome.send('launchPublicSession',
1700                     [this.user.username, locale, keyboardLayout]);
1701       }).bind(this));
1702     },
1704     /** @override **/
1705     initialize: function() {
1706       UserPod.prototype.initialize.call(this);
1708       id = this.user.username + '-keyboard';
1709       this.querySelector('.keyboard-select-label').htmlFor = id;
1710       this.querySelector('.keyboard-select').setAttribute('id', id);
1712       var id = this.user.username + '-language';
1713       this.querySelector('.language-select-label').htmlFor = id;
1714       var languageSelect = this.querySelector('.language-select');
1715       languageSelect.setAttribute('id', id);
1716       this.populateLanguageSelect(this.user.initialLocales,
1717                                   this.user.initialLocale,
1718                                   this.user.initialMultipleRecommendedLocales);
1719     },
1721     /** @override **/
1722     update: function() {
1723       UserPod.prototype.update.call(this);
1724       this.querySelector('.expanded-pane-name').textContent =
1725           this.user_.displayName;
1726       this.querySelector('.info').textContent =
1727           loadTimeData.getStringF('publicAccountInfoFormat',
1728                                   this.user_.enterpriseDomain);
1729     },
1731     /** @override */
1732     focusInput: function() {
1733       // Move tabIndex from the whole pod to the main input.
1734       this.tabIndex = -1;
1735       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1736       this.mainInput.focus();
1737     },
1739     /** @override */
1740     reset: function(takeFocus) {
1741       if (!takeFocus)
1742         this.expanded = false;
1743       this.enterButtonElement.disabled = false;
1744       UserPod.prototype.reset.call(this, takeFocus);
1745     },
1747     /** @override */
1748     activate: function(e) {
1749       if (!this.expanded) {
1750         this.expanded = true;
1751         this.focusInput();
1752       }
1753       return true;
1754     },
1756     /** @override */
1757     handleClickOnPod_: function(e) {
1758       if (this.parentNode.disabled)
1759         return;
1761       this.parentNode.focusPod(this);
1762       this.parentNode.setActivatedPod(this, e);
1763       // Prevent default so that we don't trigger 'focus' event.
1764       e.preventDefault();
1765     },
1767     /**
1768      * Updates the display name shown on the pod.
1769      * @param {string} displayName The new display name
1770      */
1771     setDisplayName: function(displayName) {
1772       this.user_.displayName = displayName;
1773       this.update();
1774     },
1776     /**
1777      * Handle mouse and keyboard events for the learn more button. Triggering
1778      * the button causes information about public sessions to be shown.
1779      * @param {Event} event Mouse or keyboard event.
1780      */
1781     handleLearnMoreEvent: function(event) {
1782       switch (event.type) {
1783         // Show informaton on left click. Let any other clicks propagate.
1784         case 'click':
1785           if (event.button != 0)
1786             return;
1787           break;
1788         // Show informaton when <Return> or <Space> is pressed. Let any other
1789         // key presses propagate.
1790         case 'keydown':
1791           switch (event.keyCode) {
1792             case 13:  // Return.
1793             case 32:  // Space.
1794               break;
1795             default:
1796               return;
1797           }
1798           break;
1799       }
1800       chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
1801       stopEventPropagation(event);
1802     },
1804     makeSpaceForExpandedPod_: function() {
1805       var width = this.classList.contains('advanced') ?
1806           PUBLIC_EXPANDED_ADVANCED_WIDTH : PUBLIC_EXPANDED_BASIC_WIDTH;
1807       var isDesktopUserManager = Oobe.getInstance().displayType ==
1808           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1809       var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
1810                                               POD_ROW_PADDING;
1811       if (this.left + width > $('pod-row').offsetWidth - rowPadding)
1812         this.left = $('pod-row').offsetWidth - rowPadding - width;
1813     },
1815     /**
1816      * Transition the expanded pod from the basic to the advanced view.
1817      */
1818     transitionToAdvanced_: function() {
1819       var pod = this;
1820       var languageAndInputSection =
1821           this.querySelector('.language-and-input-section');
1822       this.classList.add('transitioning-to-advanced');
1823       setTimeout(function() {
1824         pod.classList.add('advanced');
1825         pod.makeSpaceForExpandedPod_();
1826         languageAndInputSection.addEventListener('webkitTransitionEnd',
1827                                                  function observer() {
1828           languageAndInputSection.removeEventListener('webkitTransitionEnd',
1829                                                       observer);
1830           pod.classList.remove('transitioning-to-advanced');
1831           pod.querySelector('.language-select').focus();
1832         });
1833         // Guard timer set to animation duration + 20ms.
1834         ensureTransitionEndEvent(languageAndInputSection, 380);
1835       }, 0);
1836     },
1838     /**
1839      * Retrieves the list of keyboard layouts available for the currently
1840      * selected locale.
1841      */
1842     getPublicSessionKeyboardLayouts_: function() {
1843       var selectedLocale = this.querySelector('.language-select').value;
1844       if (selectedLocale ==
1845           this.querySelector('.keyboard-select').loadedLocale) {
1846         // If the list of keyboard layouts was loaded for the currently selected
1847         // locale, it is already up to date.
1848         return;
1849       }
1850       chrome.send('getPublicSessionKeyboardLayouts',
1851                   [this.user.username, selectedLocale]);
1852      },
1854     /**
1855      * Populates the keyboard layout "select" element with a list of layouts.
1856      * @param {string} locale The locale to which this list of keyboard layouts
1857      *     applies
1858      * @param {!Object} list List of available keyboard layouts
1859      */
1860     populateKeyboardSelect: function(locale, list) {
1861       if (locale != this.querySelector('.language-select').value) {
1862         // The selected locale has changed and the list of keyboard layouts is
1863         // not applicable. This method will be called again when a list of
1864         // keyboard layouts applicable to the selected locale is retrieved.
1865         return;
1866       }
1868       var keyboardSelect = this.querySelector('.keyboard-select');
1869       keyboardSelect.loadedLocale = locale;
1870       keyboardSelect.innerHTML = '';
1871       for (var i = 0; i < list.length; ++i) {
1872         var item = list[i];
1873         keyboardSelect.appendChild(
1874             new Option(item.title, item.value, item.selected, item.selected));
1875       }
1876     },
1878     /**
1879      * Populates the language "select" element with a list of locales.
1880      * @param {!Object} locales The list of available locales
1881      * @param {string} defaultLocale The locale to select by default
1882      * @param {boolean} multipleRecommendedLocales Whether |locales| contains
1883      *     two or more recommended locales
1884      */
1885     populateLanguageSelect: function(locales,
1886                                      defaultLocale,
1887                                      multipleRecommendedLocales) {
1888       var languageSelect = this.querySelector('.language-select');
1889       // If the user manually selected a locale, do not change the selection.
1890       // Otherwise, select the new |defaultLocale|.
1891       var selected =
1892           languageSelect.manuallyChanged ? languageSelect.value : defaultLocale;
1893       languageSelect.innerHTML = '';
1894       var group = languageSelect;
1895       for (var i = 0; i < locales.length; ++i) {
1896         var item = locales[i];
1897         if (item.optionGroupName) {
1898           group = document.createElement('optgroup');
1899           group.label = item.optionGroupName;
1900           languageSelect.appendChild(group);
1901         } else {
1902           group.appendChild(new Option(item.title,
1903                                        item.value,
1904                                        item.value == selected,
1905                                        item.value == selected));
1906         }
1907       }
1908       languageSelect.multipleRecommendedLocales = multipleRecommendedLocales;
1910       // Retrieve a list of keyboard layouts applicable to the locale that is
1911       // now selected.
1912       this.getPublicSessionKeyboardLayouts_();
1913     }
1914   };
1916   /**
1917    * Creates a user pod to be used only in desktop chrome.
1918    * @constructor
1919    * @extends {UserPod}
1920    */
1921   var DesktopUserPod = cr.ui.define(function() {
1922     // Don't just instantiate a UserPod(), as this will call decorate() on the
1923     // parent object, and add duplicate event listeners.
1924     var node = $('user-pod-template').cloneNode(true);
1925     node.removeAttribute('id');
1926     return node;
1927   });
1929   DesktopUserPod.prototype = {
1930     __proto__: UserPod.prototype,
1932     /** @override */
1933     get mainInput() {
1934       if (this.user.needsSignin)
1935         return this.passwordElement;
1936       else
1937         return this.nameElement;
1938     },
1940     /** @override */
1941     update: function() {
1942       this.imageElement.src = this.user.userImage;
1943       this.nameElement.textContent = this.user.displayName;
1944       this.reauthNameHintElement.textContent = this.user.displayName;
1946       var isLockedUser = this.user.needsSignin;
1947       var isLegacySupervisedUser = this.user.legacySupervisedUser;
1948       var isChildUser = this.user.childUser;
1949       this.classList.toggle('locked', isLockedUser);
1950       this.classList.toggle('legacy-supervised', isLegacySupervisedUser);
1951       this.classList.toggle('child', isChildUser);
1953       if (this.isAuthTypeUserClick)
1954         this.passwordLabelElement.textContent = this.authValue;
1956       this.actionBoxRemoveUserWarningTextElement.hidden =
1957           isLegacySupervisedUser;
1958       this.actionBoxRemoveLegacySupervisedUserWarningTextElement.hidden =
1959           !isLegacySupervisedUser;
1961       this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
1962         'passwordFieldAccessibleName', this.user_.emailAddress));
1964       UserPod.prototype.updateActionBoxArea.call(this);
1965     },
1967     /** @override */
1968     focusInput: function() {
1969       // Move tabIndex from the whole pod to the main input.
1970       this.tabIndex = -1;
1971       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1972       this.mainInput.focus();
1973     },
1975     /** @override */
1976     activate: function(e) {
1977       if (!this.user.needsSignin) {
1978         Oobe.launchUser(this.user.profilePath);
1979       } else if (!this.passwordElement.value) {
1980         return false;
1981       } else {
1982         chrome.send('authenticatedLaunchUser',
1983                     [this.user.profilePath,
1984                      this.user.emailAddress,
1985                      this.passwordElement.value]);
1986       }
1987       this.passwordElement.value = '';
1988       return true;
1989     },
1991     /** @override */
1992     handleClickOnPod_: function(e) {
1993       if (this.parentNode.disabled)
1994         return;
1996       Oobe.clearErrors();
1997       this.parentNode.lastFocusedPod_ = this;
1999       // If this is an unlocked pod, then open a browser window. Otherwise
2000       // just activate the pod and show the password field.
2001       if (!this.user.needsSignin && !this.isActionBoxMenuActive)
2002         this.activate(e);
2004       if (this.isAuthTypeUserClick)
2005         chrome.send('attemptUnlock', [this.user.emailAddress]);
2006     },
2007   };
2009   /**
2010    * Creates a user pod that represents kiosk app.
2011    * @constructor
2012    * @extends {UserPod}
2013    */
2014   var KioskAppPod = cr.ui.define(function() {
2015     var node = UserPod();
2016     return node;
2017   });
2019   KioskAppPod.prototype = {
2020     __proto__: UserPod.prototype,
2022     /** @override */
2023     decorate: function() {
2024       UserPod.prototype.decorate.call(this);
2025       this.launchAppButtonElement.addEventListener('click',
2026                                                    this.activate.bind(this));
2027     },
2029     /** @override */
2030     update: function() {
2031       this.imageElement.src = this.user.iconUrl;
2032       this.imageElement.alt = this.user.label;
2033       this.imageElement.title = this.user.label;
2034       this.passwordEntryContainerElement.hidden = true;
2035       this.launchAppButtonContainerElement.hidden = false;
2036       this.nameElement.textContent = this.user.label;
2037       this.reauthNameHintElement.textContent = this.user.label;
2039       UserPod.prototype.updateActionBoxArea.call(this);
2040       UserPod.prototype.customizeUserPodPerUserType.call(this);
2041     },
2043     /** @override */
2044     get mainInput() {
2045       return this.launchAppButtonElement;
2046     },
2048     /** @override */
2049     focusInput: function() {
2050       // Move tabIndex from the whole pod to the main input.
2051       this.tabIndex = -1;
2052       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
2053       this.mainInput.focus();
2054     },
2056     /** @override */
2057     get forceOnlineSignin() {
2058       return false;
2059     },
2061     /** @override */
2062     activate: function(e) {
2063       var diagnosticMode = e && e.ctrlKey;
2064       this.launchApp_(this.user, diagnosticMode);
2065       return true;
2066     },
2068     /** @override */
2069     handleClickOnPod_: function(e) {
2070       if (this.parentNode.disabled)
2071         return;
2073       Oobe.clearErrors();
2074       this.parentNode.lastFocusedPod_ = this;
2075       this.activate(e);
2076     },
2078     /**
2079      * Launch the app. If |diagnosticMode| is true, ask user to confirm.
2080      * @param {Object} app App data.
2081      * @param {boolean} diagnosticMode Whether to run the app in diagnostic
2082      *     mode.
2083      */
2084     launchApp_: function(app, diagnosticMode) {
2085       if (!diagnosticMode) {
2086         chrome.send('launchKioskApp', [app.id, false]);
2087         return;
2088       }
2090       var oobe = $('oobe');
2091       if (!oobe.confirmDiagnosticMode_) {
2092         oobe.confirmDiagnosticMode_ =
2093             new cr.ui.dialogs.ConfirmDialog(document.body);
2094         oobe.confirmDiagnosticMode_.setOkLabel(
2095             loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
2096         oobe.confirmDiagnosticMode_.setCancelLabel(
2097             loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
2098       }
2100       oobe.confirmDiagnosticMode_.show(
2101           loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
2102                                   app.label),
2103           function() {
2104             chrome.send('launchKioskApp', [app.id, true]);
2105           });
2106     },
2107   };
2109   /**
2110    * Creates a new pod row element.
2111    * @constructor
2112    * @extends {HTMLDivElement}
2113    */
2114   var PodRow = cr.ui.define('podrow');
2116   PodRow.prototype = {
2117     __proto__: HTMLDivElement.prototype,
2119     // Whether this user pod row is shown for the first time.
2120     firstShown_: true,
2122     // True if inside focusPod().
2123     insideFocusPod_: false,
2125     // Focused pod.
2126     focusedPod_: undefined,
2128     // Activated pod, i.e. the pod of current login attempt.
2129     activatedPod_: undefined,
2131     // Pod that was most recently focused, if any.
2132     lastFocusedPod_: undefined,
2134     // Pods whose initial images haven't been loaded yet.
2135     podsWithPendingImages_: [],
2137     // Whether pod placement has been postponed.
2138     podPlacementPostponed_: false,
2140     // Standard user pod height/width.
2141     userPodHeight_: 0,
2142     userPodWidth_: 0,
2144     // Array of apps that are shown in addition to other user pods.
2145     apps_: [],
2147     // True to show app pods along with user pods.
2148     shouldShowApps_: true,
2150     // Array of users that are shown (public/supervised/regular).
2151     users_: [],
2153     // If we're in Touch View mode.
2154     touchViewEnabled_: false,
2156     /** @override */
2157     decorate: function() {
2158       // Event listeners that are installed for the time period during which
2159       // the element is visible.
2160       this.listeners_ = {
2161         focus: [this.handleFocus_.bind(this), true /* useCapture */],
2162         click: [this.handleClick_.bind(this), true],
2163         mousemove: [this.handleMouseMove_.bind(this), false],
2164         keydown: [this.handleKeyDown.bind(this), false]
2165       };
2167       var isDesktopUserManager = Oobe.getInstance().displayType ==
2168           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2169       this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT :
2170                                                    CROS_POD_HEIGHT;
2171       // Same for Chrome OS and desktop.
2172       this.userPodWidth_ = POD_WIDTH;
2173     },
2175     /**
2176      * Returns all the pods in this pod row.
2177      * @type {NodeList}
2178      */
2179     get pods() {
2180       return Array.prototype.slice.call(this.children);
2181     },
2183     /**
2184      * Return true if user pod row has only single user pod in it, which should
2185      * always be focused except desktop and touch view modes.
2186      * @type {boolean}
2187      */
2188     get alwaysFocusSinglePod() {
2189       var isDesktopUserManager = Oobe.getInstance().displayType ==
2190           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2192       return (isDesktopUserManager || this.touchViewEnabled_) ?
2193           false : this.children.length == 1;
2194     },
2196     /**
2197      * Returns pod with the given app id.
2198      * @param {!string} app_id Application id to be matched.
2199      * @return {Object} Pod with the given app id. null if pod hasn't been
2200      *     found.
2201      */
2202     getPodWithAppId_: function(app_id) {
2203       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2204         if (pod.user.isApp && pod.user.id == app_id)
2205           return pod;
2206       }
2207       return null;
2208     },
2210     /**
2211      * Returns pod with the given username (null if there is no such pod).
2212      * @param {string} username Username to be matched.
2213      * @return {Object} Pod with the given username. null if pod hasn't been
2214      *     found.
2215      */
2216     getPodWithUsername_: function(username) {
2217       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2218         if (pod.user.username == username)
2219           return pod;
2220       }
2221       return null;
2222     },
2224     /**
2225      * True if the the pod row is disabled (handles no user interaction).
2226      * @type {boolean}
2227      */
2228     disabled_: false,
2229     get disabled() {
2230       return this.disabled_;
2231     },
2232     set disabled(value) {
2233       this.disabled_ = value;
2234       var controls = this.querySelectorAll('button,input');
2235       for (var i = 0, control; control = controls[i]; ++i) {
2236         control.disabled = value;
2237       }
2238     },
2240     /**
2241      * Creates a user pod from given email.
2242      * @param {!Object} user User info dictionary.
2243      */
2244     createUserPod: function(user) {
2245       var userPod;
2246       if (user.isDesktopUser)
2247         userPod = new DesktopUserPod({user: user});
2248       else if (user.publicAccount)
2249         userPod = new PublicAccountUserPod({user: user});
2250       else if (user.isApp)
2251         userPod = new KioskAppPod({user: user});
2252       else
2253         userPod = new UserPod({user: user});
2255       userPod.hidden = false;
2256       return userPod;
2257     },
2259     /**
2260      * Add an existing user pod to this pod row.
2261      * @param {!Object} user User info dictionary.
2262      */
2263     addUserPod: function(user) {
2264       var userPod = this.createUserPod(user);
2265       this.appendChild(userPod);
2266       userPod.initialize();
2267     },
2269     /**
2270      * Runs app with a given id from the list of loaded apps.
2271      * @param {!string} app_id of an app to run.
2272      * @param {boolean=} opt_diagnosticMode Whether to run the app in
2273      *     diagnostic mode. Default is false.
2274      */
2275     findAndRunAppForTesting: function(app_id, opt_diagnosticMode) {
2276       var app = this.getPodWithAppId_(app_id);
2277       if (app) {
2278         var activationEvent = cr.doc.createEvent('MouseEvents');
2279         var ctrlKey = opt_diagnosticMode;
2280         activationEvent.initMouseEvent('click', true, true, null,
2281             0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
2282         app.dispatchEvent(activationEvent);
2283       }
2284     },
2286     /**
2287      * Removes user pod from pod row.
2288      * @param {string} email User's email.
2289      */
2290     removeUserPod: function(username) {
2291       var podToRemove = this.getPodWithUsername_(username);
2292       if (podToRemove == null) {
2293         console.warn('Attempt to remove not existing pod for ' + username +
2294             '.');
2295         return;
2296       }
2297       this.removeChild(podToRemove);
2298       if (this.pods.length > 0)
2299         this.placePods_();
2300     },
2302     /**
2303      * Returns index of given pod or -1 if not found.
2304      * @param {UserPod} pod Pod to look up.
2305      * @private
2306      */
2307     indexOf_: function(pod) {
2308       for (var i = 0; i < this.pods.length; ++i) {
2309         if (pod == this.pods[i])
2310           return i;
2311       }
2312       return -1;
2313     },
2315     /**
2316      * Populates pod row with given existing users and start init animation.
2317      * @param {array} users Array of existing user emails.
2318      */
2319     loadPods: function(users) {
2320       this.users_ = users;
2322       this.rebuildPods();
2323     },
2325     /**
2326      * Scrolls focused user pod into view.
2327      */
2328     scrollFocusedPodIntoView: function() {
2329       var pod = this.focusedPod_;
2330       if (!pod)
2331         return;
2333       // First check whether focused pod is already fully visible.
2334       var visibleArea = $('scroll-container');
2335       // Visible area may not defined at user manager screen on all platforms.
2336       // Windows, Mac and Linux do not have visible area.
2337       if (!visibleArea)
2338         return;
2339       var scrollTop = visibleArea.scrollTop;
2340       var clientHeight = visibleArea.clientHeight;
2341       var podTop = $('oobe').offsetTop + pod.offsetTop;
2342       var padding = USER_POD_KEYBOARD_MIN_PADDING;
2343       if (podTop + pod.height + padding <= scrollTop + clientHeight &&
2344           podTop - padding >= scrollTop) {
2345         return;
2346       }
2348       // Scroll so that user pod is as centered as possible.
2349       visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2;
2350     },
2352     /**
2353      * Rebuilds pod row using users_ and apps_ that were previously set or
2354      * updated.
2355      */
2356     rebuildPods: function() {
2357       var emptyPodRow = this.pods.length == 0;
2359       // Clear existing pods.
2360       this.innerHTML = '';
2361       this.focusedPod_ = undefined;
2362       this.activatedPod_ = undefined;
2363       this.lastFocusedPod_ = undefined;
2365       // Switch off animation
2366       Oobe.getInstance().toggleClass('flying-pods', false);
2368       // Populate the pod row.
2369       for (var i = 0; i < this.users_.length; ++i)
2370         this.addUserPod(this.users_[i]);
2372       for (var i = 0, pod; pod = this.pods[i]; ++i)
2373         this.podsWithPendingImages_.push(pod);
2375       // TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
2376       if (this.shouldShowApps_) {
2377         for (var i = 0; i < this.apps_.length; ++i)
2378           this.addUserPod(this.apps_[i]);
2379       }
2381       // Make sure we eventually show the pod row, even if some image is stuck.
2382       setTimeout(function() {
2383         $('pod-row').classList.remove('images-loading');
2384       }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
2386       var isAccountPicker = $('login-header-bar').signinUIState ==
2387           SIGNIN_UI_STATE.ACCOUNT_PICKER;
2389       // Immediately recalculate pods layout only when current UI is account
2390       // picker. Otherwise postpone it.
2391       if (isAccountPicker) {
2392         this.placePods_();
2393         this.maybePreselectPod();
2395         // Without timeout changes in pods positions will be animated even
2396         // though it happened when 'flying-pods' class was disabled.
2397         setTimeout(function() {
2398           Oobe.getInstance().toggleClass('flying-pods', true);
2399         }, 0);
2400       } else {
2401         this.podPlacementPostponed_ = true;
2403         // Update [Cancel] button state.
2404         if ($('login-header-bar').signinUIState ==
2405                 SIGNIN_UI_STATE.GAIA_SIGNIN &&
2406             emptyPodRow &&
2407             this.pods.length > 0) {
2408           login.GaiaSigninScreen.updateCancelButtonState();
2409         }
2410       }
2411     },
2413     /**
2414      * Adds given apps to the pod row.
2415      * @param {array} apps Array of apps.
2416      */
2417     setApps: function(apps) {
2418       this.apps_ = apps;
2419       this.rebuildPods();
2420       chrome.send('kioskAppsLoaded');
2422       // Check whether there's a pending kiosk app error.
2423       window.setTimeout(function() {
2424         chrome.send('checkKioskAppLaunchError');
2425       }, 500);
2426     },
2428     /**
2429      * Sets whether should show app pods.
2430      * @param {boolean} shouldShowApps Whether app pods should be shown.
2431      */
2432     setShouldShowApps: function(shouldShowApps) {
2433       if (this.shouldShowApps_ == shouldShowApps)
2434         return;
2436       this.shouldShowApps_ = shouldShowApps;
2437       this.rebuildPods();
2438     },
2440     /**
2441      * Shows a custom icon on a user pod besides the input field.
2442      * @param {string} username Username of pod to add button
2443      * @param {!{id: !string,
2444      *           hardlockOnClick: boolean,
2445      *           isTrialRun: boolean,
2446      *           ariaLabel: string | undefined,
2447      *           tooltip: ({text: string, autoshow: boolean} | undefined)}} icon
2448      *     The icon parameters.
2449      */
2450     showUserPodCustomIcon: function(username, icon) {
2451       var pod = this.getPodWithUsername_(username);
2452       if (pod == null) {
2453         console.error('Unable to show user pod button: user pod not found.');
2454         return;
2455       }
2457       if (!icon.id && !icon.tooltip)
2458         return;
2460       if (icon.id)
2461         pod.customIconElement.setIcon(icon.id);
2463       if (icon.isTrialRun) {
2464         pod.customIconElement.setInteractive(
2465             this.onDidClickLockIconDuringTrialRun_.bind(this, username));
2466       } else if (icon.hardlockOnClick) {
2467         pod.customIconElement.setInteractive(
2468             this.hardlockUserPod_.bind(this, username));
2469       } else {
2470         pod.customIconElement.setInteractive(null);
2471       }
2473       var ariaLabel = icon.ariaLabel || (icon.tooltip && icon.tooltip.text);
2474       if (ariaLabel)
2475         pod.customIconElement.setAriaLabel(ariaLabel);
2476       else
2477         console.warn('No ARIA label for user pod custom icon.');
2479       pod.customIconElement.show();
2481       // This has to be called after |show| in case the tooltip should be shown
2482       // immediatelly.
2483       pod.customIconElement.setTooltip(
2484           icon.tooltip || {text: '', autoshow: false});
2485     },
2487     /**
2488      * Hard-locks user pod for the user. If user pod is hard-locked, it can be
2489      * only unlocked using password, and the authentication type cannot be
2490      * changed.
2491      * @param {!string} username The user's username.
2492      * @private
2493      */
2494     hardlockUserPod_: function(username) {
2495       chrome.send('hardlockPod', [username]);
2496     },
2498     /**
2499      * Records a metric indicating that the user clicked on the lock icon during
2500      * the trial run for Easy Unlock.
2501      * @param {!string} username The user's username.
2502      * @private
2503      */
2504     onDidClickLockIconDuringTrialRun_: function(username) {
2505       chrome.send('recordClickOnLockIcon', [username]);
2506     },
2508     /**
2509      * Hides the custom icon in the user pod added by showUserPodCustomIcon().
2510      * @param {string} username Username of pod to remove button
2511      */
2512     hideUserPodCustomIcon: function(username) {
2513       var pod = this.getPodWithUsername_(username);
2514       if (pod == null) {
2515         console.error('Unable to hide user pod button: user pod not found.');
2516         return;
2517       }
2519       // TODO(tengs): Allow option for a fading transition.
2520       pod.customIconElement.hide();
2521     },
2523     /**
2524      * Sets the authentication type used to authenticate the user.
2525      * @param {string} username Username of selected user
2526      * @param {number} authType Authentication type, must be one of the
2527      *                          values listed in AUTH_TYPE enum.
2528      * @param {string} value The initial value to use for authentication.
2529      */
2530     setAuthType: function(username, authType, value) {
2531       var pod = this.getPodWithUsername_(username);
2532       if (pod == null) {
2533         console.error('Unable to set auth type: user pod not found.');
2534         return;
2535       }
2536       pod.setAuthType(authType, value);
2537     },
2539     /**
2540      * Sets the state of touch view mode.
2541      * @param {boolean} isTouchViewEnabled true if the mode is on.
2542      */
2543     setTouchViewState: function(isTouchViewEnabled) {
2544       this.touchViewEnabled_ = isTouchViewEnabled;
2545       this.pods.forEach(function(pod, index) {
2546         pod.actionBoxAreaElement.classList.toggle('forced', isTouchViewEnabled);
2547       });
2548     },
2550     /**
2551      * Updates the display name shown on a public session pod.
2552      * @param {string} userID The user ID of the public session
2553      * @param {string} displayName The new display name
2554      */
2555     setPublicSessionDisplayName: function(userID, displayName) {
2556       var pod = this.getPodWithUsername_(userID);
2557       if (pod != null)
2558         pod.setDisplayName(displayName);
2559     },
2561     /**
2562      * Updates the list of locales available for a public session.
2563      * @param {string} userID The user ID of the public session
2564      * @param {!Object} locales The list of available locales
2565      * @param {string} defaultLocale The locale to select by default
2566      * @param {boolean} multipleRecommendedLocales Whether |locales| contains
2567      *     two or more recommended locales
2568      */
2569     setPublicSessionLocales: function(userID,
2570                                       locales,
2571                                       defaultLocale,
2572                                       multipleRecommendedLocales) {
2573       var pod = this.getPodWithUsername_(userID);
2574       if (pod != null) {
2575         pod.populateLanguageSelect(locales,
2576                                    defaultLocale,
2577                                    multipleRecommendedLocales);
2578       }
2579     },
2581     /**
2582      * Updates the list of available keyboard layouts for a public session pod.
2583      * @param {string} userID The user ID of the public session
2584      * @param {string} locale The locale to which this list of keyboard layouts
2585      *     applies
2586      * @param {!Object} list List of available keyboard layouts
2587      */
2588     setPublicSessionKeyboardLayouts: function(userID, locale, list) {
2589       var pod = this.getPodWithUsername_(userID);
2590       if (pod != null)
2591         pod.populateKeyboardSelect(locale, list);
2592     },
2594     /**
2595      * Called when window was resized.
2596      */
2597     onWindowResize: function() {
2598       var layout = this.calculateLayout_();
2599       if (layout.columns != this.columns || layout.rows != this.rows)
2600         this.placePods_();
2602       this.scrollFocusedPodIntoView();
2603     },
2605     /**
2606      * Returns width of podrow having |columns| number of columns.
2607      * @private
2608      */
2609     columnsToWidth_: function(columns) {
2610       var isDesktopUserManager = Oobe.getInstance().displayType ==
2611           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2612       var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2613                                           MARGIN_BY_COLUMNS[columns];
2614       var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2615                                               POD_ROW_PADDING;
2616       return 2 * rowPadding + columns * this.userPodWidth_ +
2617           (columns - 1) * margin;
2618     },
2620     /**
2621      * Returns height of podrow having |rows| number of rows.
2622      * @private
2623      */
2624     rowsToHeight_: function(rows) {
2625       var isDesktopUserManager = Oobe.getInstance().displayType ==
2626           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2627       var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2628                                               POD_ROW_PADDING;
2629       return 2 * rowPadding + rows * this.userPodHeight_;
2630     },
2632     /**
2633      * Calculates number of columns and rows that podrow should have in order to
2634      * hold as much its pods as possible for current screen size. Also it tries
2635      * to choose layout that looks good.
2636      * @return {{columns: number, rows: number}}
2637      */
2638     calculateLayout_: function() {
2639       var preferredColumns = this.pods.length < COLUMNS.length ?
2640           COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
2641       var maxWidth = Oobe.getInstance().clientAreaSize.width;
2642       var columns = preferredColumns;
2643       while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
2644         --columns;
2645       var rows = Math.floor((this.pods.length - 1) / columns) + 1;
2646       if (getComputedStyle(
2647           $('signin-banner'), null).getPropertyValue('display') != 'none') {
2648         rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER);
2649       }
2650       var maxHeigth = Oobe.getInstance().clientAreaSize.height;
2651       while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
2652         --rows;
2653       // One more iteration if it's not enough cells to place all pods.
2654       while (maxWidth >= this.columnsToWidth_(columns + 1) &&
2655              columns * rows < this.pods.length &&
2656              columns < MAX_NUMBER_OF_COLUMNS) {
2657          ++columns;
2658       }
2659       return {columns: columns, rows: rows};
2660     },
2662     /**
2663      * Places pods onto their positions onto pod grid.
2664      * @private
2665      */
2666     placePods_: function() {
2667       var layout = this.calculateLayout_();
2668       var columns = this.columns = layout.columns;
2669       var rows = this.rows = layout.rows;
2670       var maxPodsNumber = columns * rows;
2671       var isDesktopUserManager = Oobe.getInstance().displayType ==
2672           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2673       var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2674                                           MARGIN_BY_COLUMNS[columns];
2675       this.parentNode.setPreferredSize(
2676           this.columnsToWidth_(columns), this.rowsToHeight_(rows));
2677       var height = this.userPodHeight_;
2678       var width = this.userPodWidth_;
2679       this.pods.forEach(function(pod, index) {
2680         if (index >= maxPodsNumber) {
2681            pod.hidden = true;
2682            return;
2683         }
2684         pod.hidden = false;
2685         if (pod.offsetHeight != height) {
2686           console.error('Pod offsetHeight (' + pod.offsetHeight +
2687               ') and POD_HEIGHT (' + height + ') are not equal.');
2688         }
2689         if (pod.offsetWidth != width) {
2690           console.error('Pod offsetWidth (' + pod.offsetWidth +
2691               ') and POD_WIDTH (' + width + ') are not equal.');
2692         }
2693         var column = index % columns;
2694         var row = Math.floor(index / columns);
2695         var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2696                                                 POD_ROW_PADDING;
2697         pod.left = rowPadding + column * (width + margin);
2699         // On desktop, we want the rows to always be equally spaced.
2700         pod.top = isDesktopUserManager ? row * (height + rowPadding) :
2701                                          row * height + rowPadding;
2702       });
2703       Oobe.getInstance().updateScreenSize(this.parentNode);
2704     },
2706     /**
2707      * Number of columns.
2708      * @type {?number}
2709      */
2710     set columns(columns) {
2711       // Cannot use 'columns' here.
2712       this.setAttribute('ncolumns', columns);
2713     },
2714     get columns() {
2715       return parseInt(this.getAttribute('ncolumns'));
2716     },
2718     /**
2719      * Number of rows.
2720      * @type {?number}
2721      */
2722     set rows(rows) {
2723       // Cannot use 'rows' here.
2724       this.setAttribute('nrows', rows);
2725     },
2726     get rows() {
2727       return parseInt(this.getAttribute('nrows'));
2728     },
2730     /**
2731      * Whether the pod is currently focused.
2732      * @param {UserPod} pod Pod to check for focus.
2733      * @return {boolean} Pod focus status.
2734      */
2735     isFocused: function(pod) {
2736       return this.focusedPod_ == pod;
2737     },
2739     /**
2740      * Focuses a given user pod or clear focus when given null.
2741      * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
2742      * @param {boolean=} opt_force If true, forces focus update even when
2743      *     podToFocus is already focused.
2744      * @param {boolean=} opt_skipInputFocus If true, don't focus on the input
2745      *     box of user pod.
2746      */
2747     focusPod: function(podToFocus, opt_force, opt_skipInputFocus) {
2748       if (this.isFocused(podToFocus) && !opt_force) {
2749         // Calling focusPod w/o podToFocus means reset.
2750         if (!podToFocus)
2751           Oobe.clearErrors();
2752         return;
2753       }
2755       // Make sure there's only one focusPod operation happening at a time.
2756       if (this.insideFocusPod_) {
2757         return;
2758       }
2759       this.insideFocusPod_ = true;
2761       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2762         if (!this.alwaysFocusSinglePod) {
2763           pod.isActionBoxMenuActive = false;
2764         }
2765         if (pod != podToFocus) {
2766           pod.isActionBoxMenuHovered = false;
2767           pod.classList.remove('focused');
2768           // On Desktop, the faded style is not set correctly, so we should
2769           // manually fade out non-focused pods if there is a focused pod.
2770           if (pod.user.isDesktopUser && podToFocus)
2771             pod.classList.add('faded');
2772           else
2773             pod.classList.remove('faded');
2774           pod.reset(false);
2775         }
2776       }
2778       // Clear any error messages for previous pod.
2779       if (!this.isFocused(podToFocus))
2780         Oobe.clearErrors();
2782       var hadFocus = !!this.focusedPod_;
2783       this.focusedPod_ = podToFocus;
2784       if (podToFocus) {
2785         podToFocus.classList.remove('faded');
2786         podToFocus.classList.add('focused');
2787         if (!podToFocus.multiProfilesPolicyApplied) {
2788           podToFocus.classList.toggle('signing-in', false);
2789           if (!opt_skipInputFocus)
2790             podToFocus.focusInput();
2791         } else {
2792           podToFocus.userTypeBubbleElement.classList.add('bubble-shown');
2793           // Note it is not necessary to skip this focus request when
2794           // |opt_skipInputFocus| is true. When |multiProfilesPolicyApplied|
2795           // is false, it doesn't focus on the password input box by default.
2796           podToFocus.focus();
2797         }
2799         // focusPod() automatically loads wallpaper
2800         if (!podToFocus.user.isApp)
2801           chrome.send('focusPod', [podToFocus.user.username]);
2802         this.firstShown_ = false;
2803         this.lastFocusedPod_ = podToFocus;
2804         this.scrollFocusedPodIntoView();
2805       }
2806       this.insideFocusPod_ = false;
2807     },
2809     /**
2810      * Resets wallpaper to the last active user's wallpaper, if any.
2811      */
2812     loadLastWallpaper: function() {
2813       if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp)
2814         chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
2815     },
2817     /**
2818      * Returns the currently activated pod.
2819      * @type {UserPod}
2820      */
2821     get activatedPod() {
2822       return this.activatedPod_;
2823     },
2825     /**
2826      * Sets currently activated pod.
2827      * @param {UserPod} pod Pod to check for focus.
2828      * @param {Event} e Event object.
2829      */
2830     setActivatedPod: function(pod, e) {
2831       if (pod && pod.activate(e))
2832         this.activatedPod_ = pod;
2833     },
2835     /**
2836      * The pod of the signed-in user, if any; null otherwise.
2837      * @type {?UserPod}
2838      */
2839     get lockedPod() {
2840       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2841         if (pod.user.signedIn)
2842           return pod;
2843       }
2844       return null;
2845     },
2847     /**
2848      * The pod that is preselected on user pod row show.
2849      * @type {?UserPod}
2850      */
2851     get preselectedPod() {
2852       var isDesktopUserManager = Oobe.getInstance().displayType ==
2853           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2854       if (isDesktopUserManager) {
2855         // On desktop, don't pre-select a pod if it's the only one.
2856         if (this.pods.length == 1)
2857           return null;
2859         // The desktop User Manager can send the index of a pod that should be
2860         // initially focused in url hash.
2861         var podIndex = parseInt(window.location.hash.substr(1));
2862         if (isNaN(podIndex) || podIndex >= this.pods.length)
2863           return null;
2864         return this.pods[podIndex];
2865       }
2867       var lockedPod = this.lockedPod;
2868       if (lockedPod)
2869         return lockedPod;
2870       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2871         if (!pod.multiProfilesPolicyApplied) {
2872           return pod;
2873         }
2874       }
2875       return this.pods[0];
2876     },
2878     /**
2879      * Resets input UI.
2880      * @param {boolean} takeFocus True to take focus.
2881      */
2882     reset: function(takeFocus) {
2883       this.disabled = false;
2884       if (this.activatedPod_)
2885         this.activatedPod_.reset(takeFocus);
2886     },
2888     /**
2889      * Restores input focus to current selected pod, if there is any.
2890      */
2891     refocusCurrentPod: function() {
2892       if (this.focusedPod_ && !this.focusedPod_.multiProfilesPolicyApplied) {
2893         this.focusedPod_.focusInput();
2894       }
2895     },
2897     /**
2898      * Clears focused pod password field.
2899      */
2900     clearFocusedPod: function() {
2901       if (!this.disabled && this.focusedPod_)
2902         this.focusedPod_.reset(true);
2903     },
2905     /**
2906      * Shows signin UI.
2907      * @param {string} email Email for signin UI.
2908      */
2909     showSigninUI: function(email) {
2910       // Clear any error messages that might still be around.
2911       Oobe.clearErrors();
2912       this.disabled = true;
2913       this.lastFocusedPod_ = this.getPodWithUsername_(email);
2914       Oobe.showSigninUI(email);
2915     },
2917     /**
2918      * Updates current image of a user.
2919      * @param {string} username User for which to update the image.
2920      */
2921     updateUserImage: function(username) {
2922       var pod = this.getPodWithUsername_(username);
2923       if (pod)
2924         pod.updateUserImage();
2925     },
2927     /**
2928      * Handler of click event.
2929      * @param {Event} e Click Event object.
2930      * @private
2931      */
2932     handleClick_: function(e) {
2933       if (this.disabled)
2934         return;
2936       // Clear all menus if the click is outside pod menu and its
2937       // button area.
2938       if (!findAncestorByClass(e.target, 'action-box-menu') &&
2939           !findAncestorByClass(e.target, 'action-box-area')) {
2940         for (var i = 0, pod; pod = this.pods[i]; ++i)
2941           pod.isActionBoxMenuActive = false;
2942       }
2944       // Clears focus if not clicked on a pod and if there's more than one pod.
2945       var pod = findAncestorByClass(e.target, 'pod');
2946       if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) {
2947         this.focusPod();
2948       }
2950       if (pod)
2951         pod.isActionBoxMenuHovered = true;
2953       // Return focus back to single pod.
2954       if (this.alwaysFocusSinglePod && !pod) {
2955         if ($('login-header-bar').contains(e.target))
2956           return;
2957         this.focusPod(this.focusedPod_, true /* force */);
2958         this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
2959         this.focusedPod_.isActionBoxMenuHovered = false;
2960       }
2961     },
2963     /**
2964      * Handler of mouse move event.
2965      * @param {Event} e Click Event object.
2966      * @private
2967      */
2968     handleMouseMove_: function(e) {
2969       if (this.disabled)
2970         return;
2971       if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
2972         return;
2974       // Defocus (thus hide) action box, if it is focused on a user pod
2975       // and the pointer is not hovering over it.
2976       var pod = findAncestorByClass(e.target, 'pod');
2977       if (document.activeElement &&
2978           document.activeElement.parentNode != pod &&
2979           document.activeElement.classList.contains('action-box-area')) {
2980         document.activeElement.parentNode.focus();
2981       }
2983       if (pod)
2984         pod.isActionBoxMenuHovered = true;
2986       // Hide action boxes on other user pods.
2987       for (var i = 0, p; p = this.pods[i]; ++i)
2988         if (p != pod && !p.isActionBoxMenuActive)
2989           p.isActionBoxMenuHovered = false;
2990     },
2992     /**
2993      * Handles focus event.
2994      * @param {Event} e Focus Event object.
2995      * @private
2996      */
2997     handleFocus_: function(e) {
2998       if (this.disabled)
2999         return;
3000       if (e.target.parentNode == this) {
3001         // Focus on a pod
3002         if (e.target.classList.contains('focused')) {
3003           if (!e.target.multiProfilesPolicyApplied)
3004             e.target.focusInput();
3005           else
3006             e.target.userTypeBubbleElement.classList.add('bubble-shown');
3007         } else
3008           this.focusPod(e.target);
3009         return;
3010       }
3012       var pod = findAncestorByClass(e.target, 'pod');
3013       if (pod && pod.parentNode == this) {
3014         // Focus on a control of a pod but not on the action area button.
3015         if (!pod.classList.contains('focused')) {
3016           if (e.target.classList.contains('action-box-area') ||
3017               e.target.classList.contains('remove-warning-button')) {
3018             // focusPod usually moves focus on the password input box which
3019             // triggers virtual keyboard to show up. But the focus may move to a
3020             // non text input element shortly by e.target.focus. Hence, a
3021             // virtual keyboard flicking might be observed. We need to manually
3022             // prevent focus on password input box to avoid virtual keyboard
3023             // flicking in this case. See crbug.com/396016 for details.
3024             this.focusPod(pod, false, true /* opt_skipInputFocus */);
3025           } else {
3026             this.focusPod(pod);
3027           }
3028           pod.userTypeBubbleElement.classList.remove('bubble-shown');
3029           e.target.focus();
3030         }
3031         return;
3032       }
3034       // Clears pod focus when we reach here. It means new focus is neither
3035       // on a pod nor on a button/input for a pod.
3036       // Do not "defocus" user pod when it is a single pod.
3037       // That means that 'focused' class will not be removed and
3038       // input field/button will always be visible.
3039       if (!this.alwaysFocusSinglePod)
3040         this.focusPod();
3041       else {
3042         // Hide user-type-bubble in case this is one pod and we lost focus of
3043         // it.
3044         this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
3045       }
3046     },
3048     /**
3049      * Handler of keydown event.
3050      * @param {Event} e KeyDown Event object.
3051      */
3052     handleKeyDown: function(e) {
3053       if (this.disabled)
3054         return;
3055       var editing = e.target.tagName == 'INPUT' && e.target.value;
3056       switch (e.keyIdentifier) {
3057         case 'Left':
3058           if (!editing) {
3059             if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
3060               this.focusPod(this.focusedPod_.previousElementSibling);
3061             else
3062               this.focusPod(this.lastElementChild);
3064             e.stopPropagation();
3065           }
3066           break;
3067         case 'Right':
3068           if (!editing) {
3069             if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
3070               this.focusPod(this.focusedPod_.nextElementSibling);
3071             else
3072               this.focusPod(this.firstElementChild);
3074             e.stopPropagation();
3075           }
3076           break;
3077         case 'Enter':
3078           if (this.focusedPod_) {
3079             var targetTag = e.target.tagName;
3080             if (e.target == this.focusedPod_.passwordElement ||
3081                 (targetTag != 'INPUT' &&
3082                  targetTag != 'BUTTON' &&
3083                  targetTag != 'A')) {
3084               this.setActivatedPod(this.focusedPod_, e);
3085               e.stopPropagation();
3086             }
3087           }
3088           break;
3089         case 'U+001B':  // Esc
3090           if (!this.alwaysFocusSinglePod)
3091             this.focusPod();
3092           break;
3093       }
3094     },
3096     /**
3097      * Called right after the pod row is shown.
3098      */
3099     handleAfterShow: function() {
3100       // Without timeout changes in pods positions will be animated even though
3101       // it happened when 'flying-pods' class was disabled.
3102       setTimeout(function() {
3103         Oobe.getInstance().toggleClass('flying-pods', true);
3104       }, 0);
3105       // Force input focus for user pod on show and once transition ends.
3106       if (this.focusedPod_) {
3107         var focusedPod = this.focusedPod_;
3108         var screen = this.parentNode;
3109         var self = this;
3110         focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
3111           focusedPod.removeEventListener('webkitTransitionEnd', f);
3112           focusedPod.reset(true);
3113           // Notify screen that it is ready.
3114           screen.onShow();
3115         });
3116         // Guard timer for 1 second -- it would conver all possible animations.
3117         ensureTransitionEndEvent(focusedPod, 1000);
3118       }
3119     },
3121     /**
3122      * Called right before the pod row is shown.
3123      */
3124     handleBeforeShow: function() {
3125       Oobe.getInstance().toggleClass('flying-pods', false);
3126       for (var event in this.listeners_) {
3127         this.ownerDocument.addEventListener(
3128             event, this.listeners_[event][0], this.listeners_[event][1]);
3129       }
3130       $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
3132       if (this.podPlacementPostponed_) {
3133         this.podPlacementPostponed_ = false;
3134         this.placePods_();
3135         this.maybePreselectPod();
3136       }
3137     },
3139     /**
3140      * Called when the element is hidden.
3141      */
3142     handleHide: function() {
3143       for (var event in this.listeners_) {
3144         this.ownerDocument.removeEventListener(
3145             event, this.listeners_[event][0], this.listeners_[event][1]);
3146       }
3147       $('login-header-bar').buttonsTabIndex = 0;
3148     },
3150     /**
3151      * Called when a pod's user image finishes loading.
3152      */
3153     handlePodImageLoad: function(pod) {
3154       var index = this.podsWithPendingImages_.indexOf(pod);
3155       if (index == -1) {
3156         return;
3157       }
3159       this.podsWithPendingImages_.splice(index, 1);
3160       if (this.podsWithPendingImages_.length == 0) {
3161         this.classList.remove('images-loading');
3162       }
3163     },
3165     /**
3166      * Preselects pod, if needed.
3167      */
3168      maybePreselectPod: function() {
3169        var pod = this.preselectedPod;
3170        this.focusPod(pod);
3172        // Hide user-type-bubble in case all user pods are disabled and we focus
3173        // first pod.
3174        if (pod && pod.multiProfilesPolicyApplied) {
3175          pod.userTypeBubbleElement.classList.remove('bubble-shown');
3176        }
3177      }
3178   };
3180   return {
3181     PodRow: PodRow
3182   };