• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 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<include src="wallpaper_loader.js"></include>
6
7/**
8 * @fileoverview User pod row implementation.
9 */
10
11cr.define('login', function() {
12  /**
13   * Number of displayed columns depending on user pod count.
14   * @type {Array.<number>}
15   * @const
16   */
17  var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6];
18
19  /**
20   * Mapping between number of columns in pod-row and margin between user pods
21   * for such layout.
22   * @type {Array.<number>}
23   * @const
24   */
25  var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12];
26
27  /**
28   * Maximal number of columns currently supported by pod-row.
29   * @type {number}
30   * @const
31   */
32  var MAX_NUMBER_OF_COLUMNS = 6;
33
34  /**
35   * Variables used for pod placement processing.
36   * Width and height should be synced with computed CSS sizes of pods.
37   */
38  var POD_WIDTH = 180;
39  var POD_HEIGHT = 217;
40  var POD_ROW_PADDING = 10;
41
42  /**
43   * Whether to preselect the first pod automatically on login screen.
44   * @type {boolean}
45   * @const
46   */
47  var PRESELECT_FIRST_POD = true;
48
49  /**
50   * Maximum time for which the pod row remains hidden until all user images
51   * have been loaded.
52   * @type {number}
53   * @const
54   */
55  var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
56
57  /**
58   * Public session help topic identifier.
59   * @type {number}
60   * @const
61   */
62  var HELP_TOPIC_PUBLIC_SESSION = 3041033;
63
64  /**
65   * Oauth token status. These must match UserManager::OAuthTokenStatus.
66   * @enum {number}
67   * @const
68   */
69  var OAuthTokenStatus = {
70    UNKNOWN: 0,
71    INVALID_OLD: 1,
72    VALID_OLD: 2,
73    INVALID_NEW: 3,
74    VALID_NEW: 4
75  };
76
77  /**
78   * Tab order for user pods. Update these when adding new controls.
79   * @enum {number}
80   * @const
81   */
82  var UserPodTabOrder = {
83    POD_INPUT: 1,     // Password input fields (and whole pods themselves).
84    HEADER_BAR: 2,    // Buttons on the header bar (Shutdown, Add User).
85    ACTION_BOX: 3,    // Action box buttons.
86    PAD_MENU_ITEM: 4  // User pad menu items (Remove this user).
87  };
88
89  // Focus and tab order are organized as follows:
90  //
91  // (1) all user pods have tab index 1 so they are traversed first;
92  // (2) when a user pod is activated, its tab index is set to -1 and its
93  //     main input field gets focus and tab index 1;
94  // (3) buttons on the header bar have tab index 2 so they follow user pods;
95  // (4) Action box buttons have tab index 3 and follow header bar buttons;
96  // (5) lastly, focus jumps to the Status Area and back to user pods.
97  //
98  // 'Focus' event is handled by a capture handler for the whole document
99  // and in some cases 'mousedown' event handlers are used instead of 'click'
100  // handlers where it's necessary to prevent 'focus' event from being fired.
101
102  /**
103   * Helper function to remove a class from given element.
104   * @param {!HTMLElement} el Element whose class list to change.
105   * @param {string} cl Class to remove.
106   */
107  function removeClass(el, cl) {
108    el.classList.remove(cl);
109  }
110
111  /**
112   * Creates a user pod.
113   * @constructor
114   * @extends {HTMLDivElement}
115   */
116  var UserPod = cr.ui.define(function() {
117    var node = $('user-pod-template').cloneNode(true);
118    node.removeAttribute('id');
119    return node;
120  });
121
122  /**
123   * Stops event propagation from the any user pod child element.
124   * @param {Event} e Event to handle.
125   */
126  function stopEventPropagation(e) {
127    // Prevent default so that we don't trigger a 'focus' event.
128    e.preventDefault();
129    e.stopPropagation();
130  }
131
132  /**
133   * Unique salt added to user image URLs to prevent caching. Dictionary with
134   * user names as keys.
135   * @type {Object}
136   */
137  UserPod.userImageSalt_ = {};
138
139  UserPod.prototype = {
140    __proto__: HTMLDivElement.prototype,
141
142    /** @override */
143    decorate: function() {
144      this.tabIndex = UserPodTabOrder.POD_INPUT;
145      this.customButton.tabIndex = UserPodTabOrder.POD_INPUT;
146      this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
147
148      // Mousedown has to be used instead of click to be able to prevent 'focus'
149      // event later.
150      this.addEventListener('mousedown',
151          this.handleMouseDown_.bind(this));
152
153      this.signinButtonElement.addEventListener('click',
154          this.activate.bind(this));
155
156      this.actionBoxAreaElement.addEventListener('mousedown',
157                                                 stopEventPropagation);
158      this.actionBoxAreaElement.addEventListener('click',
159          this.handleActionAreaButtonClick_.bind(this));
160      this.actionBoxAreaElement.addEventListener('keydown',
161          this.handleActionAreaButtonKeyDown_.bind(this));
162
163      this.actionBoxMenuRemoveElement.addEventListener('click',
164          this.handleRemoveCommandClick_.bind(this));
165      this.actionBoxMenuRemoveElement.addEventListener('keydown',
166          this.handleRemoveCommandKeyDown_.bind(this));
167      this.actionBoxMenuRemoveElement.addEventListener('blur',
168          this.handleRemoveCommandBlur_.bind(this));
169
170      if (this.actionBoxRemoveUserWarningButtonElement) {
171        this.actionBoxRemoveUserWarningButtonElement.addEventListener(
172            'click',
173            this.handleRemoveUserConfirmationClick_.bind(this));
174      }
175
176      this.customButton.addEventListener('click',
177          this.handleCustomButtonClick_.bind(this));
178    },
179
180    /**
181     * Initializes the pod after its properties set and added to a pod row.
182     */
183    initialize: function() {
184      this.passwordElement.addEventListener('keydown',
185          this.parentNode.handleKeyDown.bind(this.parentNode));
186      this.passwordElement.addEventListener('keypress',
187          this.handlePasswordKeyPress_.bind(this));
188
189      this.imageElement.addEventListener('load',
190          this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
191    },
192
193    /**
194     * Resets tab order for pod elements to its initial state.
195     */
196    resetTabOrder: function() {
197      this.tabIndex = UserPodTabOrder.POD_INPUT;
198      this.mainInput.tabIndex = -1;
199    },
200
201    /**
202     * Handles keypress event (i.e. any textual input) on password input.
203     * @param {Event} e Keypress Event object.
204     * @private
205     */
206    handlePasswordKeyPress_: function(e) {
207      // When tabbing from the system tray a tab key press is received. Suppress
208      // this so as not to type a tab character into the password field.
209      if (e.keyCode == 9) {
210        e.preventDefault();
211        return;
212      }
213    },
214
215    /**
216     * Top edge margin number of pixels.
217     * @type {?number}
218     */
219    set top(top) {
220      this.style.top = cr.ui.toCssPx(top);
221    },
222    /**
223     * Left edge margin number of pixels.
224     * @type {?number}
225     */
226    set left(left) {
227      this.style.left = cr.ui.toCssPx(left);
228    },
229
230    /**
231     * Gets signed in indicator element.
232     * @type {!HTMLDivElement}
233     */
234    get signedInIndicatorElement() {
235      return this.querySelector('.signed-in-indicator');
236    },
237
238    /**
239     * Gets image element.
240     * @type {!HTMLImageElement}
241     */
242    get imageElement() {
243      return this.querySelector('.user-image');
244    },
245
246    /**
247     * Gets name element.
248     * @type {!HTMLDivElement}
249     */
250    get nameElement() {
251      return this.querySelector('.name');
252    },
253
254    /**
255     * Gets password field.
256     * @type {!HTMLInputElement}
257     */
258    get passwordElement() {
259      return this.querySelector('.password');
260    },
261
262    /**
263     * Gets Caps Lock hint image.
264     * @type {!HTMLImageElement}
265     */
266    get capslockHintElement() {
267      return this.querySelector('.capslock-hint');
268    },
269
270    /**
271     * Gets user signin button.
272     * @type {!HTMLInputElement}
273     */
274    get signinButtonElement() {
275      return this.querySelector('.signin-button');
276    },
277
278    /**
279     * Gets action box area.
280     * @type {!HTMLInputElement}
281     */
282    get actionBoxAreaElement() {
283      return this.querySelector('.action-box-area');
284    },
285
286    /**
287     * Gets user type icon area.
288     * @type {!HTMLInputElement}
289     */
290    get userTypeIconAreaElement() {
291      return this.querySelector('.user-type-icon-area');
292    },
293
294    /**
295     * Gets action box menu.
296     * @type {!HTMLInputElement}
297     */
298    get actionBoxMenuElement() {
299      return this.querySelector('.action-box-menu');
300    },
301
302    /**
303     * Gets action box menu title.
304     * @type {!HTMLInputElement}
305     */
306    get actionBoxMenuTitleElement() {
307      return this.querySelector('.action-box-menu-title');
308    },
309
310    /**
311     * Gets action box menu title, user name item.
312     * @type {!HTMLInputElement}
313     */
314    get actionBoxMenuTitleNameElement() {
315      return this.querySelector('.action-box-menu-title-name');
316    },
317
318    /**
319     * Gets action box menu title, user email item.
320     * @type {!HTMLInputElement}
321     */
322    get actionBoxMenuTitleEmailElement() {
323      return this.querySelector('.action-box-menu-title-email');
324    },
325
326    /**
327     * Gets action box menu, remove user command item.
328     * @type {!HTMLInputElement}
329     */
330    get actionBoxMenuCommandElement() {
331      return this.querySelector('.action-box-menu-remove-command');
332    },
333
334    /**
335     * Gets action box menu, remove user command item div.
336     * @type {!HTMLInputElement}
337     */
338    get actionBoxMenuRemoveElement() {
339      return this.querySelector('.action-box-menu-remove');
340    },
341
342    /**
343     * Gets action box menu, remove user command item div.
344     * @type {!HTMLInputElement}
345     */
346    get actionBoxRemoveUserWarningElement() {
347      return this.querySelector('.action-box-remove-user-warning');
348    },
349
350    /**
351     * Gets action box menu, remove user command item div.
352     * @type {!HTMLInputElement}
353     */
354    get actionBoxRemoveUserWarningButtonElement() {
355      return this.querySelector(
356          '.remove-warning-button');
357    },
358
359    /**
360     * Gets the locked user indicator box.
361     * @type {!HTMLInputElement}
362     */
363    get lockedIndicatorElement() {
364      return this.querySelector('.locked-indicator');
365    },
366
367    /**
368     * Gets the custom button. This button is normally hidden, but can be
369     * shown using the chrome.screenlockPrivate API.
370     * @type {!HTMLInputElement}
371     */
372    get customButton() {
373      return this.querySelector('.custom-button');
374    },
375
376    /**
377     * Updates the user pod element.
378     */
379    update: function() {
380      this.imageElement.src = 'chrome://userimage/' + this.user.username +
381          '?id=' + UserPod.userImageSalt_[this.user.username];
382
383      this.nameElement.textContent = this.user_.displayName;
384      this.signedInIndicatorElement.hidden = !this.user_.signedIn;
385
386      var needSignin = this.needSignin;
387      this.passwordElement.hidden = needSignin;
388      this.signinButtonElement.hidden = !needSignin;
389
390      this.updateActionBoxArea();
391    },
392
393    updateActionBoxArea: function() {
394      this.actionBoxAreaElement.hidden = this.user_.publicAccount;
395      this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
396
397      this.actionBoxAreaElement.setAttribute(
398          'aria-label', loadTimeData.getStringF(
399              'podMenuButtonAccessibleName', this.user_.emailAddress));
400      this.actionBoxMenuRemoveElement.setAttribute(
401          'aria-label', loadTimeData.getString(
402               'podMenuRemoveItemAccessibleName'));
403      this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
404          loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
405          this.user_.displayName;
406      this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
407      this.actionBoxMenuTitleEmailElement.hidden =
408          this.user_.locallyManagedUser;
409
410      this.actionBoxMenuCommandElement.textContent =
411          loadTimeData.getString('removeUser');
412      this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
413          'passwordFieldAccessibleName', this.user_.emailAddress));
414      this.userTypeIconAreaElement.hidden = !this.user_.locallyManagedUser;
415    },
416
417    /**
418     * The user that this pod represents.
419     * @type {!Object}
420     */
421    user_: undefined,
422    get user() {
423      return this.user_;
424    },
425    set user(userDict) {
426      this.user_ = userDict;
427      this.update();
428    },
429
430    /**
431     * Whether signin is required for this user.
432     */
433    get needSignin() {
434      // Signin is performed if the user has an invalid oauth token and is
435      // not currently signed in (i.e. not the lock screen).
436      return this.user.oauthTokenStatus != OAuthTokenStatus.VALID_OLD &&
437          this.user.oauthTokenStatus != OAuthTokenStatus.VALID_NEW &&
438          !this.user.signedIn;
439    },
440
441    /**
442     * Gets main input element.
443     * @type {(HTMLButtonElement|HTMLInputElement)}
444     */
445    get mainInput() {
446      if (!this.signinButtonElement.hidden)
447        return this.signinButtonElement;
448      else
449        return this.passwordElement;
450    },
451
452    /**
453     * Whether action box button is in active state.
454     * @type {boolean}
455     */
456    get isActionBoxMenuActive() {
457      return this.actionBoxAreaElement.classList.contains('active');
458    },
459    set isActionBoxMenuActive(active) {
460      if (active == this.isActionBoxMenuActive)
461        return;
462
463      if (active) {
464        this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
465        if (this.actionBoxRemoveUserWarningElement)
466          this.actionBoxRemoveUserWarningElement.hidden = true;
467
468        // Clear focus first if another pod is focused.
469        if (!this.parentNode.isFocused(this)) {
470          this.parentNode.focusPod(undefined, true);
471          this.actionBoxAreaElement.focus();
472        }
473        this.actionBoxAreaElement.classList.add('active');
474      } else {
475        this.actionBoxAreaElement.classList.remove('active');
476      }
477    },
478
479    /**
480     * Whether action box button is in hovered state.
481     * @type {boolean}
482     */
483    get isActionBoxMenuHovered() {
484      return this.actionBoxAreaElement.classList.contains('hovered');
485    },
486    set isActionBoxMenuHovered(hovered) {
487      if (hovered == this.isActionBoxMenuHovered)
488        return;
489
490      if (hovered) {
491        this.actionBoxAreaElement.classList.add('hovered');
492        this.classList.add('hovered');
493      } else {
494        this.actionBoxAreaElement.classList.remove('hovered');
495        this.classList.remove('hovered');
496      }
497    },
498
499    /**
500     * Updates the image element of the user.
501     */
502    updateUserImage: function() {
503      UserPod.userImageSalt_[this.user.username] = new Date().getTime();
504      this.update();
505    },
506
507    /**
508     * Focuses on input element.
509     */
510    focusInput: function() {
511      var needSignin = this.needSignin;
512      this.signinButtonElement.hidden = !needSignin;
513      this.passwordElement.hidden = needSignin;
514
515      // Move tabIndex from the whole pod to the main input.
516      this.tabIndex = -1;
517      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
518      this.mainInput.focus();
519    },
520
521    /**
522     * Activates the pod.
523     * @return {boolean} True if activated successfully.
524     */
525    activate: function() {
526      if (!this.signinButtonElement.hidden) {
527        this.showSigninUI();
528      } else if (!this.passwordElement.value) {
529        return false;
530      } else {
531        Oobe.disableSigninUI();
532        chrome.send('authenticateUser',
533                    [this.user.username, this.passwordElement.value]);
534      }
535
536      return true;
537    },
538
539    showSupervisedUserSigninWarning: function() {
540      // Locally managed user token has been invalidated.
541      // Make sure that pod is focused i.e. "Sign in" button is seen.
542      this.parentNode.focusPod(this);
543
544      var error = document.createElement('div');
545      var messageDiv = document.createElement('div');
546      messageDiv.className = 'error-message-bubble';
547      messageDiv.textContent =
548          loadTimeData.getString('supervisedUserExpiredTokenWarning');
549      error.appendChild(messageDiv);
550
551      $('bubble').showContentForElement(
552          this.signinButtonElement,
553          cr.ui.Bubble.Attachment.TOP,
554          error,
555          this.signinButtonElement.offsetWidth / 2,
556          4);
557    },
558
559    /**
560     * Shows signin UI for this user.
561     */
562    showSigninUI: function() {
563      if (this.user.locallyManagedUser) {
564        this.showSupervisedUserSigninWarning();
565      } else {
566        this.parentNode.showSigninUI(this.user.emailAddress);
567      }
568    },
569
570    /**
571     * Resets the input field and updates the tab order of pod controls.
572     * @param {boolean} takeFocus If true, input field takes focus.
573     */
574    reset: function(takeFocus) {
575      this.passwordElement.value = '';
576      if (takeFocus)
577        this.focusInput();  // This will set a custom tab order.
578      else
579        this.resetTabOrder();
580    },
581
582    /**
583     * Handles a click event on action area button.
584     * @param {Event} e Click event.
585     */
586    handleActionAreaButtonClick_: function(e) {
587      if (this.parentNode.disabled)
588        return;
589      this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
590    },
591
592    /**
593     * Handles a keydown event on action area button.
594     * @param {Event} e KeyDown event.
595     */
596    handleActionAreaButtonKeyDown_: function(e) {
597      if (this.disabled)
598        return;
599      switch (e.keyIdentifier) {
600        case 'Enter':
601        case 'U+0020':  // Space
602          if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
603            this.isActionBoxMenuActive = true;
604          e.stopPropagation();
605          break;
606        case 'Up':
607        case 'Down':
608          if (this.isActionBoxMenuActive) {
609            this.actionBoxMenuRemoveElement.tabIndex =
610                UserPodTabOrder.PAD_MENU_ITEM;
611            this.actionBoxMenuRemoveElement.focus();
612          }
613          e.stopPropagation();
614          break;
615        case 'U+001B':  // Esc
616          this.isActionBoxMenuActive = false;
617          e.stopPropagation();
618          break;
619        case 'U+0009':  // Tab
620          this.parentNode.focusPod();
621        default:
622          this.isActionBoxMenuActive = false;
623          break;
624      }
625    },
626
627    /**
628     * Handles a click event on remove user command.
629     * @param {Event} e Click event.
630     */
631    handleRemoveCommandClick_: function(e) {
632      if (this.user.locallyManagedUser || this.user.isDesktopUser) {
633        this.showRemoveWarning_();
634        return;
635      }
636      if (this.isActionBoxMenuActive)
637        chrome.send('removeUser', [this.user.username]);
638    },
639
640    /**
641     * Shows remove warning for managed users.
642     */
643    showRemoveWarning_: function() {
644      this.actionBoxMenuRemoveElement.hidden = true;
645      this.actionBoxRemoveUserWarningElement.hidden = false;
646    },
647
648    /**
649     * Handles a click event on remove user confirmation button.
650     * @param {Event} e Click event.
651     */
652    handleRemoveUserConfirmationClick_: function(e) {
653      if (this.isActionBoxMenuActive)
654        chrome.send('removeUser', [this.user.username]);
655    },
656
657    /**
658     * Handles a keydown event on remove command.
659     * @param {Event} e KeyDown event.
660     */
661    handleRemoveCommandKeyDown_: function(e) {
662      if (this.disabled)
663        return;
664      switch (e.keyIdentifier) {
665        case 'Enter':
666          chrome.send('removeUser', [this.user.username]);
667          e.stopPropagation();
668          break;
669        case 'Up':
670        case 'Down':
671          e.stopPropagation();
672          break;
673        case 'U+001B':  // Esc
674          this.actionBoxAreaElement.focus();
675          this.isActionBoxMenuActive = false;
676          e.stopPropagation();
677          break;
678        default:
679          this.actionBoxAreaElement.focus();
680          this.isActionBoxMenuActive = false;
681          break;
682      }
683    },
684
685    /**
686     * Handles a blur event on remove command.
687     * @param {Event} e Blur event.
688     */
689    handleRemoveCommandBlur_: function(e) {
690      if (this.disabled)
691        return;
692      this.actionBoxMenuRemoveElement.tabIndex = -1;
693    },
694
695    /**
696     * Handles mousedown event on a user pod.
697     * @param {Event} e Mousedown event.
698     */
699    handleMouseDown_: function(e) {
700      if (this.parentNode.disabled)
701        return;
702
703      if (!this.signinButtonElement.hidden && !this.isActionBoxMenuActive) {
704        this.showSigninUI();
705        // Prevent default so that we don't trigger 'focus' event.
706        e.preventDefault();
707      }
708    },
709
710    /**
711     * Called when the custom button is clicked.
712     */
713    handleCustomButtonClick_: function() {
714      chrome.send('customButtonClicked', [this.user.username]);
715    }
716  };
717
718  /**
719   * Creates a public account user pod.
720   * @constructor
721   * @extends {UserPod}
722   */
723  var PublicAccountUserPod = cr.ui.define(function() {
724    var node = UserPod();
725
726    var extras = $('public-account-user-pod-extras-template').children;
727    for (var i = 0; i < extras.length; ++i) {
728      var el = extras[i].cloneNode(true);
729      node.appendChild(el);
730    }
731
732    return node;
733  });
734
735  PublicAccountUserPod.prototype = {
736    __proto__: UserPod.prototype,
737
738    /**
739     * "Enter" button in expanded side pane.
740     * @type {!HTMLButtonElement}
741     */
742    get enterButtonElement() {
743      return this.querySelector('.enter-button');
744    },
745
746    /**
747     * Boolean flag of whether the pod is showing the side pane. The flag
748     * controls whether 'expanded' class is added to the pod's class list and
749     * resets tab order because main input element changes when the 'expanded'
750     * state changes.
751     * @type {boolean}
752     */
753    get expanded() {
754      return this.classList.contains('expanded');
755    },
756    set expanded(expanded) {
757      if (this.expanded == expanded)
758        return;
759
760      this.resetTabOrder();
761      this.classList.toggle('expanded', expanded);
762
763      var self = this;
764      this.classList.add('animating');
765      this.addEventListener('webkitTransitionEnd', function f(e) {
766        self.removeEventListener('webkitTransitionEnd', f);
767        self.classList.remove('animating');
768
769        // Accessibility focus indicator does not move with the focused
770        // element. Sends a 'focus' event on the currently focused element
771        // so that accessibility focus indicator updates its location.
772        if (document.activeElement)
773          document.activeElement.dispatchEvent(new Event('focus'));
774      });
775    },
776
777    /** @override */
778    get needSignin() {
779      return false;
780    },
781
782    /** @override */
783    get mainInput() {
784      if (this.expanded)
785        return this.enterButtonElement;
786      else
787        return this.nameElement;
788    },
789
790    /** @override */
791    decorate: function() {
792      UserPod.prototype.decorate.call(this);
793
794      this.classList.remove('need-password');
795      this.classList.add('public-account');
796
797      this.nameElement.addEventListener('keydown', (function(e) {
798        if (e.keyIdentifier == 'Enter') {
799          this.parentNode.activatedPod = this;
800          // Stop this keydown event from bubbling up to PodRow handler.
801          e.stopPropagation();
802          // Prevent default so that we don't trigger a 'click' event on the
803          // newly focused "Enter" button.
804          e.preventDefault();
805        }
806      }).bind(this));
807
808      var learnMore = this.querySelector('.learn-more');
809      learnMore.addEventListener('mousedown', stopEventPropagation);
810      learnMore.addEventListener('click', this.handleLearnMoreEvent);
811      learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
812
813      learnMore = this.querySelector('.side-pane-learn-more');
814      learnMore.addEventListener('click', this.handleLearnMoreEvent);
815      learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
816
817      this.enterButtonElement.addEventListener('click', (function(e) {
818        this.enterButtonElement.disabled = true;
819        chrome.send('launchPublicAccount', [this.user.username]);
820      }).bind(this));
821    },
822
823    /**
824     * Updates the user pod element.
825     */
826    update: function() {
827      UserPod.prototype.update.call(this);
828      this.querySelector('.side-pane-name').textContent =
829          this.user_.displayName;
830      this.querySelector('.info').textContent =
831          loadTimeData.getStringF('publicAccountInfoFormat',
832                                  this.user_.enterpriseDomain);
833    },
834
835    /** @override */
836    focusInput: function() {
837      // Move tabIndex from the whole pod to the main input.
838      this.tabIndex = -1;
839      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
840      this.mainInput.focus();
841    },
842
843    /** @override */
844    reset: function(takeFocus) {
845      if (!takeFocus)
846        this.expanded = false;
847      this.enterButtonElement.disabled = false;
848      UserPod.prototype.reset.call(this, takeFocus);
849    },
850
851    /** @override */
852    activate: function() {
853      this.expanded = true;
854      this.focusInput();
855      return true;
856    },
857
858    /** @override */
859    handleMouseDown_: function(e) {
860      if (this.parentNode.disabled)
861        return;
862
863      this.parentNode.focusPod(this);
864      this.parentNode.activatedPod = this;
865      // Prevent default so that we don't trigger 'focus' event.
866      e.preventDefault();
867    },
868
869    /**
870     * Handle mouse and keyboard events for the learn more button.
871     * Triggering the button causes information about public sessions to be
872     * shown.
873     * @param {Event} event Mouse or keyboard event.
874     */
875    handleLearnMoreEvent: function(event) {
876      switch (event.type) {
877        // Show informaton on left click. Let any other clicks propagate.
878        case 'click':
879          if (event.button != 0)
880            return;
881          break;
882        // Show informaton when <Return> or <Space> is pressed. Let any other
883        // key presses propagate.
884        case 'keydown':
885          switch (event.keyCode) {
886            case 13:  // Return.
887            case 32:  // Space.
888              break;
889            default:
890              return;
891          }
892          break;
893      }
894      chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
895      stopEventPropagation(event);
896    },
897  };
898
899  /**
900   * Creates a user pod to be used only in desktop chrome.
901   * @constructor
902   * @extends {UserPod}
903   */
904  var DesktopUserPod = cr.ui.define(function() {
905    // Don't just instantiate a UserPod(), as this will call decorate() on the
906    // parent object, and add duplicate event listeners.
907    var node = $('user-pod-template').cloneNode(true);
908    node.removeAttribute('id');
909    return node;
910  });
911
912  DesktopUserPod.prototype = {
913    __proto__: UserPod.prototype,
914
915    /** @override */
916    get mainInput() {
917      if (!this.passwordElement.hidden)
918        return this.passwordElement;
919      else
920        return this.nameElement;
921    },
922
923    /** @override */
924    decorate: function() {
925      UserPod.prototype.decorate.call(this);
926    },
927
928    /** @override */
929    update: function() {
930      // TODO(noms): Use the actual profile avatar for local profiles once the
931      // new, non-pixellated avatars are available.
932      this.imageElement.src = this.user.emailAddress == '' ?
933          'chrome://theme/IDR_USER_MANAGER_DEFAULT_AVATAR' :
934          this.user.userImage;
935      this.nameElement.textContent = this.user.displayName;
936
937      var isLockedUser = this.user.needsSignin;
938      this.signinButtonElement.hidden = true;
939      this.lockedIndicatorElement.hidden = !isLockedUser;
940      this.passwordElement.hidden = !isLockedUser;
941      this.nameElement.hidden = isLockedUser;
942
943      UserPod.prototype.updateActionBoxArea.call(this);
944    },
945
946    /** @override */
947    focusInput: function() {
948      // For focused pods, display the name unless the pod is locked.
949      var isLockedUser = this.user.needsSignin;
950      this.signinButtonElement.hidden = true;
951      this.lockedIndicatorElement.hidden = !isLockedUser;
952      this.passwordElement.hidden = !isLockedUser;
953      this.nameElement.hidden = isLockedUser;
954
955      // Move tabIndex from the whole pod to the main input.
956      this.tabIndex = -1;
957      this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
958      this.mainInput.focus();
959    },
960
961    /** @override */
962    reset: function(takeFocus) {
963      // Always display the user's name for unfocused pods.
964      if (!takeFocus)
965        this.nameElement.hidden = false;
966      UserPod.prototype.reset.call(this, takeFocus);
967    },
968
969    /** @override */
970    activate: function() {
971      if (this.passwordElement.hidden) {
972        Oobe.launchUser(this.user.emailAddress, this.user.displayName);
973      } else if (!this.passwordElement.value) {
974        return false;
975      } else {
976        chrome.send('authenticatedLaunchUser',
977                    [this.user.emailAddress,
978                     this.user.displayName,
979                     this.passwordElement.value]);
980      }
981      this.passwordElement.value = '';
982      return true;
983    },
984
985    /** @override */
986    handleMouseDown_: function(e) {
987      if (this.parentNode.disabled)
988        return;
989
990      Oobe.clearErrors();
991      this.parentNode.lastFocusedPod_ = this;
992
993      // If this is an unlocked pod, then open a browser window. Otherwise
994      // just activate the pod and show the password field.
995      if (!this.user.needsSignin && !this.isActionBoxMenuActive)
996        this.activate();
997    },
998
999    /** @override */
1000    handleRemoveUserConfirmationClick_: function(e) {
1001      chrome.send('removeUser', [this.user.profilePath]);
1002    },
1003  };
1004
1005  /**
1006   * Creates a new pod row element.
1007   * @constructor
1008   * @extends {HTMLDivElement}
1009   */
1010  var PodRow = cr.ui.define('podrow');
1011
1012  PodRow.prototype = {
1013    __proto__: HTMLDivElement.prototype,
1014
1015    // Whether this user pod row is shown for the first time.
1016    firstShown_: true,
1017
1018    // True if inside focusPod().
1019    insideFocusPod_: false,
1020
1021    // Focused pod.
1022    focusedPod_: undefined,
1023
1024    // Activated pod, i.e. the pod of current login attempt.
1025    activatedPod_: undefined,
1026
1027    // Pod that was most recently focused, if any.
1028    lastFocusedPod_: undefined,
1029
1030    // Note: created only in decorate() !
1031    wallpaperLoader_: undefined,
1032
1033    // Pods whose initial images haven't been loaded yet.
1034    podsWithPendingImages_: [],
1035
1036    /** @override */
1037    decorate: function() {
1038      // Event listeners that are installed for the time period during which
1039      // the element is visible.
1040      this.listeners_ = {
1041        focus: [this.handleFocus_.bind(this), true /* useCapture */],
1042        click: [this.handleClick_.bind(this), true],
1043        mousemove: [this.handleMouseMove_.bind(this), false],
1044        keydown: [this.handleKeyDown.bind(this), false]
1045      };
1046      this.wallpaperLoader_ = new login.WallpaperLoader();
1047    },
1048
1049    /**
1050     * Returns all the pods in this pod row.
1051     * @type {NodeList}
1052     */
1053    get pods() {
1054      return Array.prototype.slice.call(this.children);
1055    },
1056
1057    /**
1058     * Return true if user pod row has only single user pod in it.
1059     * @type {boolean}
1060     */
1061    get isSinglePod() {
1062      return this.children.length == 1;
1063    },
1064
1065    /**
1066     * Returns pod with the given username (null if there is no such pod).
1067     * @param {string} username Username to be matched.
1068     * @return {Object} Pod with the given username. null if pod hasn't been
1069     *                  found.
1070     */
1071    getPodWithUsername_: function(username) {
1072      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1073        if (pod.user.username == username)
1074          return pod;
1075      }
1076      return null;
1077    },
1078
1079    /**
1080     * True if the the pod row is disabled (handles no user interaction).
1081     * @type {boolean}
1082     */
1083    disabled_: false,
1084    get disabled() {
1085      return this.disabled_;
1086    },
1087    set disabled(value) {
1088      this.disabled_ = value;
1089      var controls = this.querySelectorAll('button,input');
1090      for (var i = 0, control; control = controls[i]; ++i) {
1091        control.disabled = value;
1092      }
1093    },
1094
1095    /**
1096     * Creates a user pod from given email.
1097     * @param {string} email User's email.
1098     */
1099    createUserPod: function(user) {
1100      var userPod;
1101      if (user.isDesktopUser)
1102        userPod = new DesktopUserPod({user: user});
1103      else if (user.publicAccount)
1104        userPod = new PublicAccountUserPod({user: user});
1105      else
1106        userPod = new UserPod({user: user});
1107
1108      userPod.hidden = false;
1109      return userPod;
1110    },
1111
1112    /**
1113     * Add an existing user pod to this pod row.
1114     * @param {!Object} user User info dictionary.
1115     * @param {boolean} animated Whether to use init animation.
1116     */
1117    addUserPod: function(user, animated) {
1118      var userPod = this.createUserPod(user);
1119      if (animated) {
1120        userPod.classList.add('init');
1121        userPod.nameElement.classList.add('init');
1122      }
1123
1124      this.appendChild(userPod);
1125      userPod.initialize();
1126    },
1127
1128    /**
1129     * Removes user pod from pod row.
1130     * @param {string} email User's email.
1131     */
1132    removeUserPod: function(username) {
1133      var podToRemove = this.getPodWithUsername_(username);
1134      if (podToRemove == null) {
1135        console.warn('Attempt to remove not existing pod for ' + username +
1136            '.');
1137        return;
1138      }
1139      this.removeChild(podToRemove);
1140      this.placePods_();
1141    },
1142
1143    /**
1144     * Returns index of given pod or -1 if not found.
1145     * @param {UserPod} pod Pod to look up.
1146     * @private
1147     */
1148    indexOf_: function(pod) {
1149      for (var i = 0; i < this.pods.length; ++i) {
1150        if (pod == this.pods[i])
1151          return i;
1152      }
1153      return -1;
1154    },
1155
1156    /**
1157     * Start first time show animation.
1158     */
1159    startInitAnimation: function() {
1160      // Schedule init animation.
1161      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1162        window.setTimeout(removeClass, 500 + i * 70, pod, 'init');
1163        window.setTimeout(removeClass, 700 + i * 70, pod.nameElement, 'init');
1164      }
1165    },
1166
1167    /**
1168     * Start login success animation.
1169     */
1170    startAuthenticatedAnimation: function() {
1171      var activated = this.indexOf_(this.activatedPod_);
1172      if (activated == -1)
1173        return;
1174
1175      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1176        if (i < activated)
1177          pod.classList.add('left');
1178        else if (i > activated)
1179          pod.classList.add('right');
1180        else
1181          pod.classList.add('zoom');
1182      }
1183    },
1184
1185    /**
1186     * Populates pod row with given existing users and start init animation.
1187     * @param {array} users Array of existing user emails.
1188     * @param {boolean} animated Whether to use init animation.
1189     */
1190    loadPods: function(users, animated) {
1191      // Clear existing pods.
1192      this.innerHTML = '';
1193      this.focusedPod_ = undefined;
1194      this.activatedPod_ = undefined;
1195      this.lastFocusedPod_ = undefined;
1196
1197      // Switch off animation
1198      Oobe.getInstance().toggleClass('flying-pods', false);
1199
1200      // Populate the pod row.
1201      for (var i = 0; i < users.length; ++i) {
1202        this.addUserPod(users[i], animated);
1203      }
1204      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1205        this.podsWithPendingImages_.push(pod);
1206      }
1207      // Make sure we eventually show the pod row, even if some image is stuck.
1208      setTimeout(function() {
1209        $('pod-row').classList.remove('images-loading');
1210      }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
1211
1212      this.placePods_();
1213
1214      // Without timeout changes in pods positions will be animated even though
1215      // it happened when 'flying-pods' class was disabled.
1216      setTimeout(function() {
1217        Oobe.getInstance().toggleClass('flying-pods', true);
1218      }, 0);
1219
1220      this.focusPod(this.preselectedPod);
1221    },
1222
1223    /**
1224     * Shows a button on a user pod with an icon. Clicking on this button
1225     * triggers an event used by the chrome.screenlockPrivate API.
1226     * @param {string} username Username of pod to add button
1227     * @param {string} iconURL URL of the button icon
1228     */
1229    showUserPodButton: function(username, iconURL) {
1230      var pod = this.getPodWithUsername_(username);
1231      if (pod == null) {
1232        console.error('Unable to show user pod button for ' + username +
1233                      ': user pod not found.');
1234        return;
1235      }
1236
1237      pod.customButton.hidden = false;
1238      var icon =
1239          pod.customButton.querySelector('.custom-button-icon');
1240      icon.src = iconURL;
1241    },
1242
1243    /**
1244     * Called when window was resized.
1245     */
1246    onWindowResize: function() {
1247      var layout = this.calculateLayout_();
1248      if (layout.columns != this.columns || layout.rows != this.rows)
1249        this.placePods_();
1250    },
1251
1252    /**
1253     * Returns width of podrow having |columns| number of columns.
1254     * @private
1255     */
1256    columnsToWidth_: function(columns) {
1257      var margin = MARGIN_BY_COLUMNS[columns];
1258      return 2 * POD_ROW_PADDING + columns * POD_WIDTH + (columns - 1) * margin;
1259    },
1260
1261    /**
1262     * Returns height of podrow having |rows| number of rows.
1263     * @private
1264     */
1265    rowsToHeight_: function(rows) {
1266      return 2 * POD_ROW_PADDING + rows * POD_HEIGHT;
1267    },
1268
1269    /**
1270     * Calculates number of columns and rows that podrow should have in order to
1271     * hold as much its pods as possible for current screen size. Also it tries
1272     * to choose layout that looks good.
1273     * @return {{columns: number, rows: number}}
1274     */
1275    calculateLayout_: function() {
1276      var preferredColumns = this.pods.length < COLUMNS.length ?
1277          COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
1278      var maxWidth = Oobe.getInstance().clientAreaSize.width;
1279      var columns = preferredColumns;
1280      while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
1281        --columns;
1282      var rows = Math.floor((this.pods.length - 1) / columns) + 1;
1283      var maxHeigth = Oobe.getInstance().clientAreaSize.height;
1284      while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
1285        --rows;
1286      // One more iteration if it's not enough cells to place all pods.
1287      while (maxWidth >= this.columnsToWidth_(columns + 1) &&
1288             columns * rows < this.pods.length &&
1289             columns < MAX_NUMBER_OF_COLUMNS) {
1290         ++columns;
1291      }
1292      return {columns: columns, rows: rows};
1293    },
1294
1295    /**
1296     * Places pods onto their positions onto pod grid.
1297     * @private
1298     */
1299    placePods_: function() {
1300      var layout = this.calculateLayout_();
1301      var columns = this.columns = layout.columns;
1302      var rows = this.rows = layout.rows;
1303      var maxPodsNumber = columns * rows;
1304      var margin = MARGIN_BY_COLUMNS[columns];
1305      this.parentNode.setPreferredSize(
1306          this.columnsToWidth_(columns), this.rowsToHeight_(rows));
1307      this.pods.forEach(function(pod, index) {
1308        if (pod.offsetHeight != POD_HEIGHT)
1309          console.error('Pod offsetHeight and POD_HEIGHT are not equal.');
1310        if (pod.offsetWidth != POD_WIDTH)
1311          console.error('Pod offsetWidht and POD_WIDTH are not equal.');
1312        if (index >= maxPodsNumber) {
1313           pod.hidden = true;
1314           return;
1315        }
1316        pod.hidden = false;
1317        var column = index % columns;
1318        var row = Math.floor(index / columns);
1319        pod.left = POD_ROW_PADDING + column * (POD_WIDTH + margin);
1320        pod.top = POD_ROW_PADDING + row * POD_HEIGHT;
1321      });
1322      Oobe.getInstance().updateScreenSize(this.parentNode);
1323    },
1324
1325    /**
1326     * Number of columns.
1327     * @type {?number}
1328     */
1329    set columns(columns) {
1330      // Cannot use 'columns' here.
1331      this.setAttribute('ncolumns', columns);
1332    },
1333    get columns() {
1334      return this.getAttribute('ncolumns');
1335    },
1336
1337    /**
1338     * Number of rows.
1339     * @type {?number}
1340     */
1341    set rows(rows) {
1342      // Cannot use 'rows' here.
1343      this.setAttribute('nrows', rows);
1344    },
1345    get rows() {
1346      return this.getAttribute('nrows');
1347    },
1348
1349    /**
1350     * Whether the pod is currently focused.
1351     * @param {UserPod} pod Pod to check for focus.
1352     * @return {boolean} Pod focus status.
1353     */
1354    isFocused: function(pod) {
1355      return this.focusedPod_ == pod;
1356    },
1357
1358    /**
1359     * Focuses a given user pod or clear focus when given null.
1360     * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
1361     * @param {boolean=} opt_force If true, forces focus update even when
1362     *                             podToFocus is already focused.
1363     */
1364    focusPod: function(podToFocus, opt_force) {
1365      if (this.isFocused(podToFocus) && !opt_force) {
1366        this.keyboardActivated_ = false;
1367        return;
1368      }
1369
1370      // Make sure there's only one focusPod operation happening at a time.
1371      if (this.insideFocusPod_) {
1372        this.keyboardActivated_ = false;
1373        return;
1374      }
1375      this.insideFocusPod_ = true;
1376
1377      this.wallpaperLoader_.reset();
1378      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1379        if (!this.isSinglePod) {
1380          pod.isActionBoxMenuActive = false;
1381        }
1382        if (pod != podToFocus) {
1383          pod.isActionBoxMenuHovered = false;
1384          pod.classList.remove('focused');
1385          pod.classList.remove('faded');
1386          pod.reset(false);
1387        }
1388      }
1389
1390      // Clear any error messages for previous pod.
1391      if (!this.isFocused(podToFocus))
1392        Oobe.clearErrors();
1393
1394      var hadFocus = !!this.focusedPod_;
1395      this.focusedPod_ = podToFocus;
1396      if (podToFocus) {
1397        podToFocus.classList.remove('faded');
1398        podToFocus.classList.add('focused');
1399        podToFocus.reset(true);  // Reset and give focus.
1400        chrome.send('focusPod', [podToFocus.user.username]);
1401
1402        this.wallpaperLoader_.scheduleLoad(podToFocus.user.username, opt_force);
1403        this.firstShown_ = false;
1404        this.lastFocusedPod_ = podToFocus;
1405      }
1406      this.insideFocusPod_ = false;
1407      this.keyboardActivated_ = false;
1408    },
1409
1410    /**
1411     * Focuses a given user pod by index or clear focus when given null.
1412     * @param {int=} podToFocus index of User pod to focus.
1413     * @param {boolean=} opt_force If true, forces focus update even when
1414     *                             podToFocus is already focused.
1415     */
1416    focusPodByIndex: function(podToFocus, opt_force) {
1417      if (podToFocus < this.pods.length)
1418        this.focusPod(this.pods[podToFocus], opt_force);
1419    },
1420
1421    /**
1422     * Resets wallpaper to the last active user's wallpaper, if any.
1423     */
1424    loadLastWallpaper: function() {
1425      if (this.lastFocusedPod_)
1426        this.wallpaperLoader_.scheduleLoad(this.lastFocusedPod_.user.username,
1427                                           true /* force */);
1428    },
1429
1430    /**
1431     * Handles 'onWallpaperLoaded' event. Recalculates statistics and
1432     * [re]schedules next wallpaper load.
1433     */
1434    onWallpaperLoaded: function(username) {
1435      this.wallpaperLoader_.onWallpaperLoaded(username);
1436    },
1437
1438    /**
1439     * Returns the currently activated pod.
1440     * @type {UserPod}
1441     */
1442    get activatedPod() {
1443      return this.activatedPod_;
1444    },
1445    set activatedPod(pod) {
1446      if (pod && pod.activate())
1447        this.activatedPod_ = pod;
1448    },
1449
1450    /**
1451     * The pod of the signed-in user, if any; null otherwise.
1452     * @type {?UserPod}
1453     */
1454    get lockedPod() {
1455      for (var i = 0, pod; pod = this.pods[i]; ++i) {
1456        if (pod.user.signedIn)
1457          return pod;
1458      }
1459      return null;
1460    },
1461
1462    /**
1463     * The pod that is preselected on user pod row show.
1464     * @type {?UserPod}
1465     */
1466    get preselectedPod() {
1467      var lockedPod = this.lockedPod;
1468      var preselectedPod = PRESELECT_FIRST_POD ?
1469          lockedPod || this.pods[0] : lockedPod;
1470      return preselectedPod;
1471    },
1472
1473    /**
1474     * Resets input UI.
1475     * @param {boolean} takeFocus True to take focus.
1476     */
1477    reset: function(takeFocus) {
1478      this.disabled = false;
1479      if (this.activatedPod_)
1480        this.activatedPod_.reset(takeFocus);
1481    },
1482
1483    /**
1484     * Restores input focus to current selected pod, if there is any.
1485     */
1486    refocusCurrentPod: function() {
1487      if (this.focusedPod_) {
1488        this.focusedPod_.focusInput();
1489      }
1490    },
1491
1492    /**
1493     * Clears focused pod password field.
1494     */
1495    clearFocusedPod: function() {
1496      if (!this.disabled && this.focusedPod_)
1497        this.focusedPod_.reset(true);
1498    },
1499
1500    /**
1501     * Shows signin UI.
1502     * @param {string} email Email for signin UI.
1503     */
1504    showSigninUI: function(email) {
1505      // Clear any error messages that might still be around.
1506      Oobe.clearErrors();
1507      this.disabled = true;
1508      this.lastFocusedPod_ = this.getPodWithUsername_(email);
1509      Oobe.showSigninUI(email);
1510    },
1511
1512    /**
1513     * Updates current image of a user.
1514     * @param {string} username User for which to update the image.
1515     */
1516    updateUserImage: function(username) {
1517      var pod = this.getPodWithUsername_(username);
1518      if (pod)
1519        pod.updateUserImage();
1520    },
1521
1522    /**
1523     * Resets OAuth token status (invalidates it).
1524     * @param {string} username User for which to reset the status.
1525     */
1526    resetUserOAuthTokenStatus: function(username) {
1527      var pod = this.getPodWithUsername_(username);
1528      if (pod) {
1529        pod.user.oauthTokenStatus = OAuthTokenStatus.INVALID_OLD;
1530        pod.update();
1531      } else {
1532        console.log('Failed to update Gaia state for: ' + username);
1533      }
1534    },
1535
1536    /**
1537     * Handler of click event.
1538     * @param {Event} e Click Event object.
1539     * @private
1540     */
1541    handleClick_: function(e) {
1542      if (this.disabled)
1543        return;
1544
1545      // Clear all menus if the click is outside pod menu and its
1546      // button area.
1547      if (!findAncestorByClass(e.target, 'action-box-menu') &&
1548          !findAncestorByClass(e.target, 'action-box-area')) {
1549        for (var i = 0, pod; pod = this.pods[i]; ++i)
1550          pod.isActionBoxMenuActive = false;
1551      }
1552
1553      // Clears focus if not clicked on a pod and if there's more than one pod.
1554      var pod = findAncestorByClass(e.target, 'pod');
1555      if ((!pod || pod.parentNode != this) && !this.isSinglePod) {
1556        this.focusPod();
1557      }
1558
1559      if (pod)
1560        pod.isActionBoxMenuHovered = true;
1561
1562      // Return focus back to single pod.
1563      if (this.isSinglePod) {
1564        this.focusPod(this.focusedPod_, true /* force */);
1565        if (!pod)
1566          this.focusedPod_.isActionBoxMenuHovered = false;
1567      }
1568
1569      // Also stop event propagation.
1570      if (pod && e.target == pod.imageElement)
1571        e.stopPropagation();
1572    },
1573
1574    /**
1575     * Handler of mouse move event.
1576     * @param {Event} e Click Event object.
1577     * @private
1578     */
1579    handleMouseMove_: function(e) {
1580      if (this.disabled)
1581        return;
1582      if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
1583        return;
1584
1585      // Defocus (thus hide) action box, if it is focused on a user pod
1586      // and the pointer is not hovering over it.
1587      var pod = findAncestorByClass(e.target, 'pod');
1588      if (document.activeElement &&
1589          document.activeElement.parentNode != pod &&
1590          document.activeElement.classList.contains('action-box-area')) {
1591        document.activeElement.parentNode.focus();
1592      }
1593
1594      if (pod)
1595        pod.isActionBoxMenuHovered = true;
1596
1597      // Hide action boxes on other user pods.
1598      for (var i = 0, p; p = this.pods[i]; ++i)
1599        if (p != pod && !p.isActionBoxMenuActive)
1600          p.isActionBoxMenuHovered = false;
1601    },
1602
1603    /**
1604     * Handles focus event.
1605     * @param {Event} e Focus Event object.
1606     * @private
1607     */
1608    handleFocus_: function(e) {
1609      if (this.disabled)
1610        return;
1611      if (e.target.parentNode == this) {
1612        // Focus on a pod
1613        if (e.target.classList.contains('focused'))
1614          e.target.focusInput();
1615        else
1616          this.focusPod(e.target);
1617        return;
1618      }
1619
1620      var pod = findAncestorByClass(e.target, 'pod');
1621      if (pod && pod.parentNode == this) {
1622        // Focus on a control of a pod but not on the action area button.
1623        if (!pod.classList.contains('focused') &&
1624            !e.target.classList.contains('action-box-button')) {
1625          this.focusPod(pod);
1626          e.target.focus();
1627        }
1628        return;
1629      }
1630
1631      // Clears pod focus when we reach here. It means new focus is neither
1632      // on a pod nor on a button/input for a pod.
1633      // Do not "defocus" user pod when it is a single pod.
1634      // That means that 'focused' class will not be removed and
1635      // input field/button will always be visible.
1636      if (!this.isSinglePod)
1637        this.focusPod();
1638    },
1639
1640    /**
1641     * Handler of keydown event.
1642     * @param {Event} e KeyDown Event object.
1643     */
1644    handleKeyDown: function(e) {
1645      if (this.disabled)
1646        return;
1647      var editing = e.target.tagName == 'INPUT' && e.target.value;
1648      switch (e.keyIdentifier) {
1649        case 'Left':
1650          if (!editing) {
1651            this.keyboardActivated_ = true;
1652            if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
1653              this.focusPod(this.focusedPod_.previousElementSibling);
1654            else
1655              this.focusPod(this.lastElementChild);
1656
1657            e.stopPropagation();
1658          }
1659          break;
1660        case 'Right':
1661          if (!editing) {
1662            this.keyboardActivated_ = true;
1663            if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
1664              this.focusPod(this.focusedPod_.nextElementSibling);
1665            else
1666              this.focusPod(this.firstElementChild);
1667
1668            e.stopPropagation();
1669          }
1670          break;
1671        case 'Enter':
1672          if (this.focusedPod_) {
1673            this.activatedPod = this.focusedPod_;
1674            e.stopPropagation();
1675          }
1676          break;
1677        case 'U+001B':  // Esc
1678          if (!this.isSinglePod)
1679            this.focusPod();
1680          break;
1681      }
1682    },
1683
1684    /**
1685     * Called right after the pod row is shown.
1686     */
1687    handleAfterShow: function() {
1688      // Without timeout changes in pods positions will be animated even though
1689      // it happened when 'flying-pods' class was disabled.
1690      setTimeout(function() {
1691        Oobe.getInstance().toggleClass('flying-pods', true);
1692      }, 0);
1693      // Force input focus for user pod on show and once transition ends.
1694      if (this.focusedPod_) {
1695        var focusedPod = this.focusedPod_;
1696        var screen = this.parentNode;
1697        var self = this;
1698        focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
1699          if (e.target == focusedPod) {
1700            focusedPod.removeEventListener('webkitTransitionEnd', f);
1701            focusedPod.reset(true);
1702            // Notify screen that it is ready.
1703            screen.onShow();
1704            self.wallpaperLoader_.scheduleLoad(focusedPod.user.username,
1705                                               true /* force */);
1706          }
1707        });
1708        // Guard timer for 1 second -- it would conver all possible animations.
1709        ensureTransitionEndEvent(focusedPod, 1000);
1710      }
1711    },
1712
1713    /**
1714     * Called right before the pod row is shown.
1715     */
1716    handleBeforeShow: function() {
1717      Oobe.getInstance().toggleClass('flying-pods', false);
1718      for (var event in this.listeners_) {
1719        this.ownerDocument.addEventListener(
1720            event, this.listeners_[event][0], this.listeners_[event][1]);
1721      }
1722      $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
1723    },
1724
1725    /**
1726     * Called when the element is hidden.
1727     */
1728    handleHide: function() {
1729      for (var event in this.listeners_) {
1730        this.ownerDocument.removeEventListener(
1731            event, this.listeners_[event][0], this.listeners_[event][1]);
1732      }
1733      $('login-header-bar').buttonsTabIndex = 0;
1734    },
1735
1736    /**
1737     * Called when a pod's user image finishes loading.
1738     */
1739    handlePodImageLoad: function(pod) {
1740      var index = this.podsWithPendingImages_.indexOf(pod);
1741      if (index == -1) {
1742        return;
1743      }
1744
1745      this.podsWithPendingImages_.splice(index, 1);
1746      if (this.podsWithPendingImages_.length == 0) {
1747        this.classList.remove('images-loading');
1748      }
1749    }
1750  };
1751
1752  return {
1753    PodRow: PodRow
1754  };
1755});
1756