Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / ui / login / account_picker / user_pod_row.js
blob6115625876b80feb04eddd91c5c6add47165e425
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     initialize: function() {
1934       if (this.user.needsSignin) {
1935         if (this.user.hasLocalCreds) {
1936           this.user.initialAuthType = AUTH_TYPE.OFFLINE_PASSWORD;
1937         } else {
1938           this.user.initialAuthType = AUTH_TYPE.ONLINE_SIGN_IN;
1939         }
1940       }
1941       UserPod.prototype.initialize.call(this);
1942     },
1944     /** @override */
1945     update: function() {
1946       this.imageElement.src = this.user.userImage;
1947       this.nameElement.textContent = this.user.displayName;
1948       this.reauthNameHintElement.textContent = this.user.displayName;
1950       var isLockedUser = this.user.needsSignin;
1951       var isLegacySupervisedUser = this.user.legacySupervisedUser;
1952       var isChildUser = this.user.childUser;
1953       this.classList.toggle('locked', isLockedUser);
1954       this.classList.toggle('legacy-supervised', isLegacySupervisedUser);
1955       this.classList.toggle('child', isChildUser);
1957       if (this.isAuthTypeUserClick)
1958         this.passwordLabelElement.textContent = this.authValue;
1960       this.actionBoxRemoveUserWarningTextElement.hidden =
1961           isLegacySupervisedUser;
1962       this.actionBoxRemoveLegacySupervisedUserWarningTextElement.hidden =
1963           !isLegacySupervisedUser;
1965       this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
1966         'passwordFieldAccessibleName', this.user_.emailAddress));
1968       UserPod.prototype.updateActionBoxArea.call(this);
1969     },
1971     /** @override */
1972     activate: function(e) {
1973       if (!this.user.needsSignin) {
1974         Oobe.launchUser(this.user.profilePath);
1975       } else if (this.user.hasLocalCreds && !this.passwordElement.value) {
1976         return false;
1977       } else {
1978         chrome.send('authenticatedLaunchUser',
1979                     [this.user.profilePath,
1980                      this.user.emailAddress,
1981                      this.passwordElement.value]);
1982       }
1983       this.passwordElement.value = '';
1984       return true;
1985     },
1987     /** @override */
1988     handleClickOnPod_: function(e) {
1989       if (this.parentNode.disabled)
1990         return;
1992       Oobe.clearErrors();
1993       this.parentNode.lastFocusedPod_ = this;
1995       // If this is a locked pod and there are local credentials, show the
1996       // password field.  Otherwise call activate() which will open up a browser
1997       // window or show the reauth dialog, as needed.
1998       if (!(this.user.needsSignin && this.user.hasLocalCreds) &&
1999           !this.isActionBoxMenuActive) {
2000         this.activate(e);
2001       }
2003       if (this.isAuthTypeUserClick)
2004         chrome.send('attemptUnlock', [this.user.emailAddress]);
2005     },
2006   };
2008   /**
2009    * Creates a user pod that represents kiosk app.
2010    * @constructor
2011    * @extends {UserPod}
2012    */
2013   var KioskAppPod = cr.ui.define(function() {
2014     var node = UserPod();
2015     return node;
2016   });
2018   KioskAppPod.prototype = {
2019     __proto__: UserPod.prototype,
2021     /** @override */
2022     decorate: function() {
2023       UserPod.prototype.decorate.call(this);
2024       this.launchAppButtonElement.addEventListener('click',
2025                                                    this.activate.bind(this));
2026     },
2028     /** @override */
2029     update: function() {
2030       this.imageElement.src = this.user.iconUrl;
2031       this.imageElement.alt = this.user.label;
2032       this.imageElement.title = this.user.label;
2033       this.passwordEntryContainerElement.hidden = true;
2034       this.launchAppButtonContainerElement.hidden = false;
2035       this.nameElement.textContent = this.user.label;
2036       this.reauthNameHintElement.textContent = this.user.label;
2038       UserPod.prototype.updateActionBoxArea.call(this);
2039       UserPod.prototype.customizeUserPodPerUserType.call(this);
2040     },
2042     /** @override */
2043     get mainInput() {
2044       return this.launchAppButtonElement;
2045     },
2047     /** @override */
2048     focusInput: function() {
2049       // Move tabIndex from the whole pod to the main input.
2050       this.tabIndex = -1;
2051       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
2052       this.mainInput.focus();
2053     },
2055     /** @override */
2056     get forceOnlineSignin() {
2057       return false;
2058     },
2060     /** @override */
2061     activate: function(e) {
2062       var diagnosticMode = e && e.ctrlKey;
2063       this.launchApp_(this.user, diagnosticMode);
2064       return true;
2065     },
2067     /** @override */
2068     handleClickOnPod_: function(e) {
2069       if (this.parentNode.disabled)
2070         return;
2072       Oobe.clearErrors();
2073       this.parentNode.lastFocusedPod_ = this;
2074       this.activate(e);
2075     },
2077     /**
2078      * Launch the app. If |diagnosticMode| is true, ask user to confirm.
2079      * @param {Object} app App data.
2080      * @param {boolean} diagnosticMode Whether to run the app in diagnostic
2081      *     mode.
2082      */
2083     launchApp_: function(app, diagnosticMode) {
2084       if (!diagnosticMode) {
2085         chrome.send('launchKioskApp', [app.id, false]);
2086         return;
2087       }
2089       var oobe = $('oobe');
2090       if (!oobe.confirmDiagnosticMode_) {
2091         oobe.confirmDiagnosticMode_ =
2092             new cr.ui.dialogs.ConfirmDialog(document.body);
2093         oobe.confirmDiagnosticMode_.setOkLabel(
2094             loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
2095         oobe.confirmDiagnosticMode_.setCancelLabel(
2096             loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
2097       }
2099       oobe.confirmDiagnosticMode_.show(
2100           loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
2101                                   app.label),
2102           function() {
2103             chrome.send('launchKioskApp', [app.id, true]);
2104           });
2105     },
2106   };
2108   /**
2109    * Creates a new pod row element.
2110    * @constructor
2111    * @extends {HTMLDivElement}
2112    */
2113   var PodRow = cr.ui.define('podrow');
2115   PodRow.prototype = {
2116     __proto__: HTMLDivElement.prototype,
2118     // Whether this user pod row is shown for the first time.
2119     firstShown_: true,
2121     // True if inside focusPod().
2122     insideFocusPod_: false,
2124     // Focused pod.
2125     focusedPod_: undefined,
2127     // Activated pod, i.e. the pod of current login attempt.
2128     activatedPod_: undefined,
2130     // Pod that was most recently focused, if any.
2131     lastFocusedPod_: undefined,
2133     // Pods whose initial images haven't been loaded yet.
2134     podsWithPendingImages_: [],
2136     // Whether pod placement has been postponed.
2137     podPlacementPostponed_: false,
2139     // Standard user pod height/width.
2140     userPodHeight_: 0,
2141     userPodWidth_: 0,
2143     // Array of apps that are shown in addition to other user pods.
2144     apps_: [],
2146     // True to show app pods along with user pods.
2147     shouldShowApps_: true,
2149     // Array of users that are shown (public/supervised/regular).
2150     users_: [],
2152     // If we're in Touch View mode.
2153     touchViewEnabled_: false,
2155     /** @override */
2156     decorate: function() {
2157       // Event listeners that are installed for the time period during which
2158       // the element is visible.
2159       this.listeners_ = {
2160         focus: [this.handleFocus_.bind(this), true /* useCapture */],
2161         click: [this.handleClick_.bind(this), true],
2162         mousemove: [this.handleMouseMove_.bind(this), false],
2163         keydown: [this.handleKeyDown.bind(this), false]
2164       };
2166       var isDesktopUserManager = Oobe.getInstance().displayType ==
2167           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2168       this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT :
2169                                                    CROS_POD_HEIGHT;
2170       // Same for Chrome OS and desktop.
2171       this.userPodWidth_ = POD_WIDTH;
2172     },
2174     /**
2175      * Returns all the pods in this pod row.
2176      * @type {NodeList}
2177      */
2178     get pods() {
2179       return Array.prototype.slice.call(this.children);
2180     },
2182     /**
2183      * Return true if user pod row has only single user pod in it, which should
2184      * always be focused except desktop and touch view modes.
2185      * @type {boolean}
2186      */
2187     get alwaysFocusSinglePod() {
2188       var isDesktopUserManager = Oobe.getInstance().displayType ==
2189           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2191       return (isDesktopUserManager || this.touchViewEnabled_) ?
2192           false : this.children.length == 1;
2193     },
2195     /**
2196      * Returns pod with the given app id.
2197      * @param {!string} app_id Application id to be matched.
2198      * @return {Object} Pod with the given app id. null if pod hasn't been
2199      *     found.
2200      */
2201     getPodWithAppId_: function(app_id) {
2202       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2203         if (pod.user.isApp && pod.user.id == app_id)
2204           return pod;
2205       }
2206       return null;
2207     },
2209     /**
2210      * Returns pod with the given username (null if there is no such pod).
2211      * @param {string} username Username to be matched.
2212      * @return {Object} Pod with the given username. null if pod hasn't been
2213      *     found.
2214      */
2215     getPodWithUsername_: function(username) {
2216       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2217         if (pod.user.username == username)
2218           return pod;
2219       }
2220       return null;
2221     },
2223     /**
2224      * True if the the pod row is disabled (handles no user interaction).
2225      * @type {boolean}
2226      */
2227     disabled_: false,
2228     get disabled() {
2229       return this.disabled_;
2230     },
2231     set disabled(value) {
2232       this.disabled_ = value;
2233       var controls = this.querySelectorAll('button,input');
2234       for (var i = 0, control; control = controls[i]; ++i) {
2235         control.disabled = value;
2236       }
2237     },
2239     /**
2240      * Creates a user pod from given email.
2241      * @param {!Object} user User info dictionary.
2242      */
2243     createUserPod: function(user) {
2244       var userPod;
2245       if (user.isDesktopUser)
2246         userPod = new DesktopUserPod({user: user});
2247       else if (user.publicAccount)
2248         userPod = new PublicAccountUserPod({user: user});
2249       else if (user.isApp)
2250         userPod = new KioskAppPod({user: user});
2251       else
2252         userPod = new UserPod({user: user});
2254       userPod.hidden = false;
2255       return userPod;
2256     },
2258     /**
2259      * Add an existing user pod to this pod row.
2260      * @param {!Object} user User info dictionary.
2261      */
2262     addUserPod: function(user) {
2263       var userPod = this.createUserPod(user);
2264       this.appendChild(userPod);
2265       userPod.initialize();
2266     },
2268     /**
2269      * Runs app with a given id from the list of loaded apps.
2270      * @param {!string} app_id of an app to run.
2271      * @param {boolean=} opt_diagnosticMode Whether to run the app in
2272      *     diagnostic mode. Default is false.
2273      */
2274     findAndRunAppForTesting: function(app_id, opt_diagnosticMode) {
2275       var app = this.getPodWithAppId_(app_id);
2276       if (app) {
2277         var activationEvent = cr.doc.createEvent('MouseEvents');
2278         var ctrlKey = opt_diagnosticMode;
2279         activationEvent.initMouseEvent('click', true, true, null,
2280             0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
2281         app.dispatchEvent(activationEvent);
2282       }
2283     },
2285     /**
2286      * Removes user pod from pod row.
2287      * @param {string} email User's email.
2288      */
2289     removeUserPod: function(username) {
2290       var podToRemove = this.getPodWithUsername_(username);
2291       if (podToRemove == null) {
2292         console.warn('Attempt to remove not existing pod for ' + username +
2293             '.');
2294         return;
2295       }
2296       this.removeChild(podToRemove);
2297       if (this.pods.length > 0)
2298         this.placePods_();
2299     },
2301     /**
2302      * Returns index of given pod or -1 if not found.
2303      * @param {UserPod} pod Pod to look up.
2304      * @private
2305      */
2306     indexOf_: function(pod) {
2307       for (var i = 0; i < this.pods.length; ++i) {
2308         if (pod == this.pods[i])
2309           return i;
2310       }
2311       return -1;
2312     },
2314     /**
2315      * Populates pod row with given existing users and start init animation.
2316      * @param {array} users Array of existing user emails.
2317      */
2318     loadPods: function(users) {
2319       this.users_ = users;
2321       this.rebuildPods();
2322     },
2324     /**
2325      * Scrolls focused user pod into view.
2326      */
2327     scrollFocusedPodIntoView: function() {
2328       var pod = this.focusedPod_;
2329       if (!pod)
2330         return;
2332       // First check whether focused pod is already fully visible.
2333       var visibleArea = $('scroll-container');
2334       // Visible area may not defined at user manager screen on all platforms.
2335       // Windows, Mac and Linux do not have visible area.
2336       if (!visibleArea)
2337         return;
2338       var scrollTop = visibleArea.scrollTop;
2339       var clientHeight = visibleArea.clientHeight;
2340       var podTop = $('oobe').offsetTop + pod.offsetTop;
2341       var padding = USER_POD_KEYBOARD_MIN_PADDING;
2342       if (podTop + pod.height + padding <= scrollTop + clientHeight &&
2343           podTop - padding >= scrollTop) {
2344         return;
2345       }
2347       // Scroll so that user pod is as centered as possible.
2348       visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2;
2349     },
2351     /**
2352      * Rebuilds pod row using users_ and apps_ that were previously set or
2353      * updated.
2354      */
2355     rebuildPods: function() {
2356       var emptyPodRow = this.pods.length == 0;
2358       // Clear existing pods.
2359       this.innerHTML = '';
2360       this.focusedPod_ = undefined;
2361       this.activatedPod_ = undefined;
2362       this.lastFocusedPod_ = undefined;
2364       // Switch off animation
2365       Oobe.getInstance().toggleClass('flying-pods', false);
2367       // Populate the pod row.
2368       for (var i = 0; i < this.users_.length; ++i)
2369         this.addUserPod(this.users_[i]);
2371       for (var i = 0, pod; pod = this.pods[i]; ++i)
2372         this.podsWithPendingImages_.push(pod);
2374       // TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
2375       if (this.shouldShowApps_) {
2376         for (var i = 0; i < this.apps_.length; ++i)
2377           this.addUserPod(this.apps_[i]);
2378       }
2380       // Make sure we eventually show the pod row, even if some image is stuck.
2381       setTimeout(function() {
2382         $('pod-row').classList.remove('images-loading');
2383       }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
2385       var isAccountPicker = $('login-header-bar').signinUIState ==
2386           SIGNIN_UI_STATE.ACCOUNT_PICKER;
2388       // Immediately recalculate pods layout only when current UI is account
2389       // picker. Otherwise postpone it.
2390       if (isAccountPicker) {
2391         this.placePods_();
2392         this.maybePreselectPod();
2394         // Without timeout changes in pods positions will be animated even
2395         // though it happened when 'flying-pods' class was disabled.
2396         setTimeout(function() {
2397           Oobe.getInstance().toggleClass('flying-pods', true);
2398         }, 0);
2399       } else {
2400         this.podPlacementPostponed_ = true;
2402         // Update [Cancel] button state.
2403         if ($('login-header-bar').signinUIState ==
2404                 SIGNIN_UI_STATE.GAIA_SIGNIN &&
2405             emptyPodRow &&
2406             this.pods.length > 0) {
2407           login.GaiaSigninScreen.updateCancelButtonState();
2408         }
2409       }
2410     },
2412     /**
2413      * Adds given apps to the pod row.
2414      * @param {array} apps Array of apps.
2415      */
2416     setApps: function(apps) {
2417       this.apps_ = apps;
2418       this.rebuildPods();
2419       chrome.send('kioskAppsLoaded');
2421       // Check whether there's a pending kiosk app error.
2422       window.setTimeout(function() {
2423         chrome.send('checkKioskAppLaunchError');
2424       }, 500);
2425     },
2427     /**
2428      * Sets whether should show app pods.
2429      * @param {boolean} shouldShowApps Whether app pods should be shown.
2430      */
2431     setShouldShowApps: function(shouldShowApps) {
2432       if (this.shouldShowApps_ == shouldShowApps)
2433         return;
2435       this.shouldShowApps_ = shouldShowApps;
2436       this.rebuildPods();
2437     },
2439     /**
2440      * Shows a custom icon on a user pod besides the input field.
2441      * @param {string} username Username of pod to add button
2442      * @param {!{id: !string,
2443      *           hardlockOnClick: boolean,
2444      *           isTrialRun: boolean,
2445      *           ariaLabel: string | undefined,
2446      *           tooltip: ({text: string, autoshow: boolean} | undefined)}} icon
2447      *     The icon parameters.
2448      */
2449     showUserPodCustomIcon: function(username, icon) {
2450       var pod = this.getPodWithUsername_(username);
2451       if (pod == null) {
2452         console.error('Unable to show user pod button: user pod not found.');
2453         return;
2454       }
2456       if (!icon.id && !icon.tooltip)
2457         return;
2459       if (icon.id)
2460         pod.customIconElement.setIcon(icon.id);
2462       if (icon.isTrialRun) {
2463         pod.customIconElement.setInteractive(
2464             this.onDidClickLockIconDuringTrialRun_.bind(this, username));
2465       } else if (icon.hardlockOnClick) {
2466         pod.customIconElement.setInteractive(
2467             this.hardlockUserPod_.bind(this, username));
2468       } else {
2469         pod.customIconElement.setInteractive(null);
2470       }
2472       var ariaLabel = icon.ariaLabel || (icon.tooltip && icon.tooltip.text);
2473       if (ariaLabel)
2474         pod.customIconElement.setAriaLabel(ariaLabel);
2475       else
2476         console.warn('No ARIA label for user pod custom icon.');
2478       pod.customIconElement.show();
2480       // This has to be called after |show| in case the tooltip should be shown
2481       // immediatelly.
2482       pod.customIconElement.setTooltip(
2483           icon.tooltip || {text: '', autoshow: false});
2484     },
2486     /**
2487      * Hard-locks user pod for the user. If user pod is hard-locked, it can be
2488      * only unlocked using password, and the authentication type cannot be
2489      * changed.
2490      * @param {!string} username The user's username.
2491      * @private
2492      */
2493     hardlockUserPod_: function(username) {
2494       chrome.send('hardlockPod', [username]);
2495     },
2497     /**
2498      * Records a metric indicating that the user clicked on the lock icon during
2499      * the trial run for Easy Unlock.
2500      * @param {!string} username The user's username.
2501      * @private
2502      */
2503     onDidClickLockIconDuringTrialRun_: function(username) {
2504       chrome.send('recordClickOnLockIcon', [username]);
2505     },
2507     /**
2508      * Hides the custom icon in the user pod added by showUserPodCustomIcon().
2509      * @param {string} username Username of pod to remove button
2510      */
2511     hideUserPodCustomIcon: function(username) {
2512       var pod = this.getPodWithUsername_(username);
2513       if (pod == null) {
2514         console.error('Unable to hide user pod button: user pod not found.');
2515         return;
2516       }
2518       // TODO(tengs): Allow option for a fading transition.
2519       pod.customIconElement.hide();
2520     },
2522     /**
2523      * Sets the authentication type used to authenticate the user.
2524      * @param {string} username Username of selected user
2525      * @param {number} authType Authentication type, must be one of the
2526      *                          values listed in AUTH_TYPE enum.
2527      * @param {string} value The initial value to use for authentication.
2528      */
2529     setAuthType: function(username, authType, value) {
2530       var pod = this.getPodWithUsername_(username);
2531       if (pod == null) {
2532         console.error('Unable to set auth type: user pod not found.');
2533         return;
2534       }
2535       pod.setAuthType(authType, value);
2536     },
2538     /**
2539      * Sets the state of touch view mode.
2540      * @param {boolean} isTouchViewEnabled true if the mode is on.
2541      */
2542     setTouchViewState: function(isTouchViewEnabled) {
2543       this.touchViewEnabled_ = isTouchViewEnabled;
2544       this.pods.forEach(function(pod, index) {
2545         pod.actionBoxAreaElement.classList.toggle('forced', isTouchViewEnabled);
2546       });
2547     },
2549     /**
2550      * Updates the display name shown on a public session pod.
2551      * @param {string} userID The user ID of the public session
2552      * @param {string} displayName The new display name
2553      */
2554     setPublicSessionDisplayName: function(userID, displayName) {
2555       var pod = this.getPodWithUsername_(userID);
2556       if (pod != null)
2557         pod.setDisplayName(displayName);
2558     },
2560     /**
2561      * Updates the list of locales available for a public session.
2562      * @param {string} userID The user ID of the public session
2563      * @param {!Object} locales The list of available locales
2564      * @param {string} defaultLocale The locale to select by default
2565      * @param {boolean} multipleRecommendedLocales Whether |locales| contains
2566      *     two or more recommended locales
2567      */
2568     setPublicSessionLocales: function(userID,
2569                                       locales,
2570                                       defaultLocale,
2571                                       multipleRecommendedLocales) {
2572       var pod = this.getPodWithUsername_(userID);
2573       if (pod != null) {
2574         pod.populateLanguageSelect(locales,
2575                                    defaultLocale,
2576                                    multipleRecommendedLocales);
2577       }
2578     },
2580     /**
2581      * Updates the list of available keyboard layouts for a public session pod.
2582      * @param {string} userID The user ID of the public session
2583      * @param {string} locale The locale to which this list of keyboard layouts
2584      *     applies
2585      * @param {!Object} list List of available keyboard layouts
2586      */
2587     setPublicSessionKeyboardLayouts: function(userID, locale, list) {
2588       var pod = this.getPodWithUsername_(userID);
2589       if (pod != null)
2590         pod.populateKeyboardSelect(locale, list);
2591     },
2593     /**
2594      * Called when window was resized.
2595      */
2596     onWindowResize: function() {
2597       var layout = this.calculateLayout_();
2598       if (layout.columns != this.columns || layout.rows != this.rows)
2599         this.placePods_();
2601       this.scrollFocusedPodIntoView();
2602     },
2604     /**
2605      * Returns width of podrow having |columns| number of columns.
2606      * @private
2607      */
2608     columnsToWidth_: function(columns) {
2609       var isDesktopUserManager = Oobe.getInstance().displayType ==
2610           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2611       var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2612                                           MARGIN_BY_COLUMNS[columns];
2613       var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2614                                               POD_ROW_PADDING;
2615       return 2 * rowPadding + columns * this.userPodWidth_ +
2616           (columns - 1) * margin;
2617     },
2619     /**
2620      * Returns height of podrow having |rows| number of rows.
2621      * @private
2622      */
2623     rowsToHeight_: function(rows) {
2624       var isDesktopUserManager = Oobe.getInstance().displayType ==
2625           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2626       var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2627                                               POD_ROW_PADDING;
2628       return 2 * rowPadding + rows * this.userPodHeight_;
2629     },
2631     /**
2632      * Calculates number of columns and rows that podrow should have in order to
2633      * hold as much its pods as possible for current screen size. Also it tries
2634      * to choose layout that looks good.
2635      * @return {{columns: number, rows: number}}
2636      */
2637     calculateLayout_: function() {
2638       var preferredColumns = this.pods.length < COLUMNS.length ?
2639           COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
2640       var maxWidth = Oobe.getInstance().clientAreaSize.width;
2641       var columns = preferredColumns;
2642       while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
2643         --columns;
2644       var rows = Math.floor((this.pods.length - 1) / columns) + 1;
2645       if (getComputedStyle(
2646           $('signin-banner'), null).getPropertyValue('display') != 'none') {
2647         rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER);
2648       }
2649       var maxHeigth = Oobe.getInstance().clientAreaSize.height;
2650       while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
2651         --rows;
2652       // One more iteration if it's not enough cells to place all pods.
2653       while (maxWidth >= this.columnsToWidth_(columns + 1) &&
2654              columns * rows < this.pods.length &&
2655              columns < MAX_NUMBER_OF_COLUMNS) {
2656          ++columns;
2657       }
2658       return {columns: columns, rows: rows};
2659     },
2661     /**
2662      * Places pods onto their positions onto pod grid.
2663      * @private
2664      */
2665     placePods_: function() {
2666       var layout = this.calculateLayout_();
2667       var columns = this.columns = layout.columns;
2668       var rows = this.rows = layout.rows;
2669       var maxPodsNumber = columns * rows;
2670       var isDesktopUserManager = Oobe.getInstance().displayType ==
2671           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2672       var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2673                                           MARGIN_BY_COLUMNS[columns];
2674       this.parentNode.setPreferredSize(
2675           this.columnsToWidth_(columns), this.rowsToHeight_(rows));
2676       var height = this.userPodHeight_;
2677       var width = this.userPodWidth_;
2678       this.pods.forEach(function(pod, index) {
2679         if (index >= maxPodsNumber) {
2680            pod.hidden = true;
2681            return;
2682         }
2683         pod.hidden = false;
2684         if (pod.offsetHeight != height) {
2685           console.error('Pod offsetHeight (' + pod.offsetHeight +
2686               ') and POD_HEIGHT (' + height + ') are not equal.');
2687         }
2688         if (pod.offsetWidth != width) {
2689           console.error('Pod offsetWidth (' + pod.offsetWidth +
2690               ') and POD_WIDTH (' + width + ') are not equal.');
2691         }
2692         var column = index % columns;
2693         var row = Math.floor(index / columns);
2694         var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2695                                                 POD_ROW_PADDING;
2696         pod.left = rowPadding + column * (width + margin);
2698         // On desktop, we want the rows to always be equally spaced.
2699         pod.top = isDesktopUserManager ? row * (height + rowPadding) :
2700                                          row * height + rowPadding;
2701       });
2702       Oobe.getInstance().updateScreenSize(this.parentNode);
2703     },
2705     /**
2706      * Number of columns.
2707      * @type {?number}
2708      */
2709     set columns(columns) {
2710       // Cannot use 'columns' here.
2711       this.setAttribute('ncolumns', columns);
2712     },
2713     get columns() {
2714       return parseInt(this.getAttribute('ncolumns'));
2715     },
2717     /**
2718      * Number of rows.
2719      * @type {?number}
2720      */
2721     set rows(rows) {
2722       // Cannot use 'rows' here.
2723       this.setAttribute('nrows', rows);
2724     },
2725     get rows() {
2726       return parseInt(this.getAttribute('nrows'));
2727     },
2729     /**
2730      * Whether the pod is currently focused.
2731      * @param {UserPod} pod Pod to check for focus.
2732      * @return {boolean} Pod focus status.
2733      */
2734     isFocused: function(pod) {
2735       return this.focusedPod_ == pod;
2736     },
2738     /**
2739      * Focuses a given user pod or clear focus when given null.
2740      * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
2741      * @param {boolean=} opt_force If true, forces focus update even when
2742      *     podToFocus is already focused.
2743      * @param {boolean=} opt_skipInputFocus If true, don't focus on the input
2744      *     box of user pod.
2745      */
2746     focusPod: function(podToFocus, opt_force, opt_skipInputFocus) {
2747       if (this.isFocused(podToFocus) && !opt_force) {
2748         // Calling focusPod w/o podToFocus means reset.
2749         if (!podToFocus)
2750           Oobe.clearErrors();
2751         return;
2752       }
2754       // Make sure there's only one focusPod operation happening at a time.
2755       if (this.insideFocusPod_) {
2756         return;
2757       }
2758       this.insideFocusPod_ = true;
2760       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2761         if (!this.alwaysFocusSinglePod) {
2762           pod.isActionBoxMenuActive = false;
2763         }
2764         if (pod != podToFocus) {
2765           pod.isActionBoxMenuHovered = false;
2766           pod.classList.remove('focused');
2767           // On Desktop, the faded style is not set correctly, so we should
2768           // manually fade out non-focused pods if there is a focused pod.
2769           if (pod.user.isDesktopUser && podToFocus)
2770             pod.classList.add('faded');
2771           else
2772             pod.classList.remove('faded');
2773           pod.reset(false);
2774         }
2775       }
2777       // Clear any error messages for previous pod.
2778       if (!this.isFocused(podToFocus))
2779         Oobe.clearErrors();
2781       var hadFocus = !!this.focusedPod_;
2782       this.focusedPod_ = podToFocus;
2783       if (podToFocus) {
2784         podToFocus.classList.remove('faded');
2785         podToFocus.classList.add('focused');
2786         if (!podToFocus.multiProfilesPolicyApplied) {
2787           podToFocus.classList.toggle('signing-in', false);
2788           if (!opt_skipInputFocus)
2789             podToFocus.focusInput();
2790         } else {
2791           podToFocus.userTypeBubbleElement.classList.add('bubble-shown');
2792           // Note it is not necessary to skip this focus request when
2793           // |opt_skipInputFocus| is true. When |multiProfilesPolicyApplied|
2794           // is false, it doesn't focus on the password input box by default.
2795           podToFocus.focus();
2796         }
2798         // focusPod() automatically loads wallpaper
2799         if (!podToFocus.user.isApp)
2800           chrome.send('focusPod', [podToFocus.user.username]);
2801         this.firstShown_ = false;
2802         this.lastFocusedPod_ = podToFocus;
2803         this.scrollFocusedPodIntoView();
2804       }
2805       this.insideFocusPod_ = false;
2806     },
2808     /**
2809      * Resets wallpaper to the last active user's wallpaper, if any.
2810      */
2811     loadLastWallpaper: function() {
2812       if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp)
2813         chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
2814     },
2816     /**
2817      * Returns the currently activated pod.
2818      * @type {UserPod}
2819      */
2820     get activatedPod() {
2821       return this.activatedPod_;
2822     },
2824     /**
2825      * Sets currently activated pod.
2826      * @param {UserPod} pod Pod to check for focus.
2827      * @param {Event} e Event object.
2828      */
2829     setActivatedPod: function(pod, e) {
2830       if (pod && pod.activate(e))
2831         this.activatedPod_ = pod;
2832     },
2834     /**
2835      * The pod of the signed-in user, if any; null otherwise.
2836      * @type {?UserPod}
2837      */
2838     get lockedPod() {
2839       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2840         if (pod.user.signedIn)
2841           return pod;
2842       }
2843       return null;
2844     },
2846     /**
2847      * The pod that is preselected on user pod row show.
2848      * @type {?UserPod}
2849      */
2850     get preselectedPod() {
2851       var isDesktopUserManager = Oobe.getInstance().displayType ==
2852           DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2853       if (isDesktopUserManager) {
2854         // On desktop, don't pre-select a pod if it's the only one.
2855         if (this.pods.length == 1)
2856           return null;
2858         // The desktop User Manager can send the index of a pod that should be
2859         // initially focused in url hash.
2860         var podIndex = parseInt(window.location.hash.substr(1));
2861         if (isNaN(podIndex) || podIndex >= this.pods.length)
2862           return null;
2863         return this.pods[podIndex];
2864       }
2866       var lockedPod = this.lockedPod;
2867       if (lockedPod)
2868         return lockedPod;
2869       for (var i = 0, pod; pod = this.pods[i]; ++i) {
2870         if (!pod.multiProfilesPolicyApplied) {
2871           return pod;
2872         }
2873       }
2874       return this.pods[0];
2875     },
2877     /**
2878      * Resets input UI.
2879      * @param {boolean} takeFocus True to take focus.
2880      */
2881     reset: function(takeFocus) {
2882       this.disabled = false;
2883       if (this.activatedPod_)
2884         this.activatedPod_.reset(takeFocus);
2885     },
2887     /**
2888      * Restores input focus to current selected pod, if there is any.
2889      */
2890     refocusCurrentPod: function() {
2891       if (this.focusedPod_ && !this.focusedPod_.multiProfilesPolicyApplied) {
2892         this.focusedPod_.focusInput();
2893       }
2894     },
2896     /**
2897      * Clears focused pod password field.
2898      */
2899     clearFocusedPod: function() {
2900       if (!this.disabled && this.focusedPod_)
2901         this.focusedPod_.reset(true);
2902     },
2904     /**
2905      * Shows signin UI.
2906      * @param {string} email Email for signin UI.
2907      */
2908     showSigninUI: function(email) {
2909       // Clear any error messages that might still be around.
2910       Oobe.clearErrors();
2911       this.disabled = true;
2912       this.lastFocusedPod_ = this.getPodWithUsername_(email);
2913       Oobe.showSigninUI(email);
2914     },
2916     /**
2917      * Updates current image of a user.
2918      * @param {string} username User for which to update the image.
2919      */
2920     updateUserImage: function(username) {
2921       var pod = this.getPodWithUsername_(username);
2922       if (pod)
2923         pod.updateUserImage();
2924     },
2926     /**
2927      * Handler of click event.
2928      * @param {Event} e Click Event object.
2929      * @private
2930      */
2931     handleClick_: function(e) {
2932       if (this.disabled)
2933         return;
2935       // Clear all menus if the click is outside pod menu and its
2936       // button area.
2937       if (!findAncestorByClass(e.target, 'action-box-menu') &&
2938           !findAncestorByClass(e.target, 'action-box-area')) {
2939         for (var i = 0, pod; pod = this.pods[i]; ++i)
2940           pod.isActionBoxMenuActive = false;
2941       }
2943       // Clears focus if not clicked on a pod and if there's more than one pod.
2944       var pod = findAncestorByClass(e.target, 'pod');
2945       if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) {
2946         this.focusPod();
2947       }
2949       if (pod)
2950         pod.isActionBoxMenuHovered = true;
2952       // Return focus back to single pod.
2953       if (this.alwaysFocusSinglePod && !pod) {
2954         if ($('login-header-bar').contains(e.target))
2955           return;
2956         this.focusPod(this.focusedPod_, true /* force */);
2957         this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
2958         this.focusedPod_.isActionBoxMenuHovered = false;
2959       }
2960     },
2962     /**
2963      * Handler of mouse move event.
2964      * @param {Event} e Click Event object.
2965      * @private
2966      */
2967     handleMouseMove_: function(e) {
2968       if (this.disabled)
2969         return;
2970       if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
2971         return;
2973       // Defocus (thus hide) action box, if it is focused on a user pod
2974       // and the pointer is not hovering over it.
2975       var pod = findAncestorByClass(e.target, 'pod');
2976       if (document.activeElement &&
2977           document.activeElement.parentNode != pod &&
2978           document.activeElement.classList.contains('action-box-area')) {
2979         document.activeElement.parentNode.focus();
2980       }
2982       if (pod)
2983         pod.isActionBoxMenuHovered = true;
2985       // Hide action boxes on other user pods.
2986       for (var i = 0, p; p = this.pods[i]; ++i)
2987         if (p != pod && !p.isActionBoxMenuActive)
2988           p.isActionBoxMenuHovered = false;
2989     },
2991     /**
2992      * Handles focus event.
2993      * @param {Event} e Focus Event object.
2994      * @private
2995      */
2996     handleFocus_: function(e) {
2997       if (this.disabled)
2998         return;
2999       if (e.target.parentNode == this) {
3000         // Focus on a pod
3001         if (e.target.classList.contains('focused')) {
3002           if (!e.target.multiProfilesPolicyApplied)
3003             e.target.focusInput();
3004           else
3005             e.target.userTypeBubbleElement.classList.add('bubble-shown');
3006         } else
3007           this.focusPod(e.target);
3008         return;
3009       }
3011       var pod = findAncestorByClass(e.target, 'pod');
3012       if (pod && pod.parentNode == this) {
3013         // Focus on a control of a pod but not on the action area button.
3014         if (!pod.classList.contains('focused')) {
3015           if (e.target.classList.contains('action-box-area') ||
3016               e.target.classList.contains('remove-warning-button')) {
3017             // focusPod usually moves focus on the password input box which
3018             // triggers virtual keyboard to show up. But the focus may move to a
3019             // non text input element shortly by e.target.focus. Hence, a
3020             // virtual keyboard flicking might be observed. We need to manually
3021             // prevent focus on password input box to avoid virtual keyboard
3022             // flicking in this case. See crbug.com/396016 for details.
3023             this.focusPod(pod, false, true /* opt_skipInputFocus */);
3024           } else {
3025             this.focusPod(pod);
3026           }
3027           pod.userTypeBubbleElement.classList.remove('bubble-shown');
3028           e.target.focus();
3029         }
3030         return;
3031       }
3033       // Clears pod focus when we reach here. It means new focus is neither
3034       // on a pod nor on a button/input for a pod.
3035       // Do not "defocus" user pod when it is a single pod.
3036       // That means that 'focused' class will not be removed and
3037       // input field/button will always be visible.
3038       if (!this.alwaysFocusSinglePod)
3039         this.focusPod();
3040       else {
3041         // Hide user-type-bubble in case this is one pod and we lost focus of
3042         // it.
3043         this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
3044       }
3045     },
3047     /**
3048      * Handler of keydown event.
3049      * @param {Event} e KeyDown Event object.
3050      */
3051     handleKeyDown: function(e) {
3052       if (this.disabled)
3053         return;
3054       var editing = e.target.tagName == 'INPUT' && e.target.value;
3055       switch (e.keyIdentifier) {
3056         case 'Left':
3057           if (!editing) {
3058             if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
3059               this.focusPod(this.focusedPod_.previousElementSibling);
3060             else
3061               this.focusPod(this.lastElementChild);
3063             e.stopPropagation();
3064           }
3065           break;
3066         case 'Right':
3067           if (!editing) {
3068             if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
3069               this.focusPod(this.focusedPod_.nextElementSibling);
3070             else
3071               this.focusPod(this.firstElementChild);
3073             e.stopPropagation();
3074           }
3075           break;
3076         case 'Enter':
3077           if (this.focusedPod_) {
3078             var targetTag = e.target.tagName;
3079             if (e.target == this.focusedPod_.passwordElement ||
3080                 (targetTag != 'INPUT' &&
3081                  targetTag != 'BUTTON' &&
3082                  targetTag != 'A')) {
3083               this.setActivatedPod(this.focusedPod_, e);
3084               e.stopPropagation();
3085             }
3086           }
3087           break;
3088         case 'U+001B':  // Esc
3089           if (!this.alwaysFocusSinglePod)
3090             this.focusPod();
3091           break;
3092       }
3093     },
3095     /**
3096      * Called right after the pod row is shown.
3097      */
3098     handleAfterShow: function() {
3099       // Without timeout changes in pods positions will be animated even though
3100       // it happened when 'flying-pods' class was disabled.
3101       setTimeout(function() {
3102         Oobe.getInstance().toggleClass('flying-pods', true);
3103       }, 0);
3104       // Force input focus for user pod on show and once transition ends.
3105       if (this.focusedPod_) {
3106         var focusedPod = this.focusedPod_;
3107         var screen = this.parentNode;
3108         var self = this;
3109         focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
3110           focusedPod.removeEventListener('webkitTransitionEnd', f);
3111           focusedPod.reset(true);
3112           // Notify screen that it is ready.
3113           screen.onShow();
3114         });
3115         // Guard timer for 1 second -- it would conver all possible animations.
3116         ensureTransitionEndEvent(focusedPod, 1000);
3117       }
3118     },
3120     /**
3121      * Called right before the pod row is shown.
3122      */
3123     handleBeforeShow: function() {
3124       Oobe.getInstance().toggleClass('flying-pods', false);
3125       for (var event in this.listeners_) {
3126         this.ownerDocument.addEventListener(
3127             event, this.listeners_[event][0], this.listeners_[event][1]);
3128       }
3129       $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
3131       if (this.podPlacementPostponed_) {
3132         this.podPlacementPostponed_ = false;
3133         this.placePods_();
3134         this.maybePreselectPod();
3135       }
3136     },
3138     /**
3139      * Called when the element is hidden.
3140      */
3141     handleHide: function() {
3142       for (var event in this.listeners_) {
3143         this.ownerDocument.removeEventListener(
3144             event, this.listeners_[event][0], this.listeners_[event][1]);
3145       }
3146       $('login-header-bar').buttonsTabIndex = 0;
3147     },
3149     /**
3150      * Called when a pod's user image finishes loading.
3151      */
3152     handlePodImageLoad: function(pod) {
3153       var index = this.podsWithPendingImages_.indexOf(pod);
3154       if (index == -1) {
3155         return;
3156       }
3158       this.podsWithPendingImages_.splice(index, 1);
3159       if (this.podsWithPendingImages_.length == 0) {
3160         this.classList.remove('images-loading');
3161       }
3162     },
3164     /**
3165      * Preselects pod, if needed.
3166      */
3167      maybePreselectPod: function() {
3168        var pod = this.preselectedPod;
3169        this.focusPod(pod);
3171        // Hide user-type-bubble in case all user pods are disabled and we focus
3172        // first pod.
3173        if (pod && pod.multiProfilesPolicyApplied) {
3174          pod.userTypeBubbleElement.classList.remove('bubble-shown');
3175        }
3176      }
3177   };
3179   return {
3180     PodRow: PodRow
3181   };