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