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