Add focusPod event to set user-preferred options (keyboard layout for example).
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / login / user_pod_row.js
blob28fdbe21a8780a35e3ba749ab591968124eaf520
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.
5 /**
6  * @fileoverview User pod row implementation.
7  */
9 cr.define('login', function() {
10   /**
11    * Number of displayed columns depending on user pod count.
12    * @type {Array.<number>}
13    * @const
14    */
15   var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6];
17   /**
18    * Whether to preselect the first pod automatically on login screen.
19    * @type {boolean}
20    * @const
21    */
22   var PRESELECT_FIRST_POD = true;
24   /**
25    * Wallpaper load delay in milliseconds.
26    * @type {number}
27    * @const
28    */
29   var WALLPAPER_LOAD_DELAY_MS = 500;
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;
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;
46   /**
47    * Public session help topic identifier.
48    * @type {number}
49    * @const
50    */
51   var HELP_TOPIC_PUBLIC_SESSION = 3041033;
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   };
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   };
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.
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   }
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   });
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   }
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_ = {};
128   UserPod.prototype = {
129     __proto__: HTMLDivElement.prototype,
131     /** @override */
132     decorate: function() {
133       this.tabIndex = UserPodTabOrder.POD_INPUT;
134       this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
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));
141       this.signinButtonElement.addEventListener('click',
142           this.activate.bind(this));
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));
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));
158       if (this.actionBoxRemoveUserWarningButtonElement) {
159         this.actionBoxRemoveUserWarningButtonElement.addEventListener(
160             'click',
161             this.handleRemoveUserConfirmationClick_.bind(this));
162       }
163     },
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));
174       this.imageElement.addEventListener('load',
175           this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
176     },
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     },
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     },
200     /**
201      * Gets signed in indicator element.
202      * @type {!HTMLDivElement}
203      */
204     get signedInIndicatorElement() {
205       return this.querySelector('.signed-in-indicator');
206     },
208     /**
209      * Gets image element.
210      * @type {!HTMLImageElement}
211      */
212     get imageElement() {
213       return this.querySelector('.user-image');
214     },
216     /**
217      * Gets name element.
218      * @type {!HTMLDivElement}
219      */
220     get nameElement() {
221       return this.querySelector('.name');
222     },
224     /**
225      * Gets password field.
226      * @type {!HTMLInputElement}
227      */
228     get passwordElement() {
229       return this.querySelector('.password');
230     },
232     /**
233      * Gets Caps Lock hint image.
234      * @type {!HTMLImageElement}
235      */
236     get capslockHintElement() {
237       return this.querySelector('.capslock-hint');
238     },
240     /**
241      * Gets user signin button.
242      * @type {!HTMLInputElement}
243      */
244     get signinButtonElement() {
245       return this.querySelector('.signin-button');
246     },
248     /**
249      * Gets action box area.
250      * @type {!HTMLInputElement}
251      */
252     get actionBoxAreaElement() {
253       return this.querySelector('.action-box-area');
254     },
256     /**
257      * Gets user type icon area.
258      * @type {!HTMLInputElement}
259      */
260     get userTypeIconAreaElement() {
261       return this.querySelector('.user-type-icon-area');
262     },
264     /**
265      * Gets action box menu.
266      * @type {!HTMLInputElement}
267      */
268     get actionBoxMenuElement() {
269       return this.querySelector('.action-box-menu');
270     },
272     /**
273      * Gets action box menu title.
274      * @type {!HTMLInputElement}
275      */
276     get actionBoxMenuTitleElement() {
277       return this.querySelector('.action-box-menu-title');
278     },
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     },
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     },
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     },
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     },
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     },
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     },
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];
336       this.nameElement.textContent = this.user_.displayName;
337       this.signedInIndicatorElement.hidden = !this.user_.signedIn;
339       var needSignin = this.needSignin;
340       this.passwordElement.hidden = needSignin;
341       this.signinButtonElement.hidden = !needSignin;
343       this.updateActionBoxArea();
344     },
346     updateActionBoxArea: function() {
347       this.actionBoxAreaElement.hidden = this.user_.publicAccount;
348       this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
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;
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     },
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     },
383     /**
384      * Whether signin is required for this user.
385      */
386     get needSignin() {
387       // 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       return this.user.oauthTokenStatus != OAuthTokenStatus.VALID_OLD &&
390           this.user.oauthTokenStatus != OAuthTokenStatus.VALID_NEW &&
391           !this.user.signedIn;
392     },
394     /**
395      * Gets main input element.
396      * @type {(HTMLButtonElement|HTMLInputElement)}
397      */
398     get mainInput() {
399       if (!this.signinButtonElement.hidden)
400         return this.signinButtonElement;
401       else
402         return this.passwordElement;
403     },
405     /**
406      * Whether action box button is in active state.
407      * @type {boolean}
408      */
409     get isActionBoxMenuActive() {
410       return this.actionBoxAreaElement.classList.contains('active');
411     },
412     set isActionBoxMenuActive(active) {
413       if (active == this.isActionBoxMenuActive)
414         return;
416       if (active) {
417         this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
418         if (this.actionBoxRemoveUserWarningElement)
419           this.actionBoxRemoveUserWarningElement.hidden = true;
421         // Clear focus first if another pod is focused.
422         if (!this.parentNode.isFocused(this)) {
423           this.parentNode.focusPod(undefined, true);
424           this.actionBoxAreaElement.focus();
425         }
426         this.actionBoxAreaElement.classList.add('active');
427       } else {
428         this.actionBoxAreaElement.classList.remove('active');
429       }
430     },
432     /**
433      * Whether action box button is in hovered state.
434      * @type {boolean}
435      */
436     get isActionBoxMenuHovered() {
437       return this.actionBoxAreaElement.classList.contains('hovered');
438     },
439     set isActionBoxMenuHovered(hovered) {
440       if (hovered == this.isActionBoxMenuHovered)
441         return;
443       if (hovered) {
444         this.actionBoxAreaElement.classList.add('hovered');
445       } else {
446         this.actionBoxAreaElement.classList.remove('hovered');
447       }
448     },
450     /**
451      * Updates the image element of the user.
452      */
453     updateUserImage: function() {
454       UserPod.userImageSalt_[this.user.username] = new Date().getTime();
455       this.update();
456     },
458     /**
459      * Focuses on input element.
460      */
461     focusInput: function() {
462       var needSignin = this.needSignin;
463       this.signinButtonElement.hidden = !needSignin;
464       this.passwordElement.hidden = needSignin;
466       // Move tabIndex from the whole pod to the main input.
467       this.tabIndex = -1;
468       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
469       this.mainInput.focus();
470     },
472     /**
473      * Activates the pod.
474      * @return {boolean} True if activated successfully.
475      */
476     activate: function() {
477       if (!this.signinButtonElement.hidden) {
478         this.showSigninUI();
479       } else if (!this.passwordElement.value) {
480         return false;
481       } else {
482         Oobe.disableSigninUI();
483         chrome.send('authenticateUser',
484                     [this.user.username, this.passwordElement.value]);
485       }
487       return true;
488     },
490     showSupervisedUserSigninWarning: function() {
491       // Locally managed user token has been invalidated.
492       // Make sure that pod is focused i.e. "Sign in" button is seen.
493       this.parentNode.focusPod(this);
494       $('bubble').showTextForElement(
495           this.signinButtonElement,
496           loadTimeData.getString('supervisedUserExpiredTokenWarning'),
497           cr.ui.Bubble.Attachment.TOP,
498           24, 4);
499     },
501     /**
502      * Shows signin UI for this user.
503      */
504     showSigninUI: function() {
505       if (this.user.locallyManagedUser) {
506         this.showSupervisedUserSigninWarning();
507       } else {
508         this.parentNode.showSigninUI(this.user.emailAddress);
509       }
510     },
512     /**
513      * Resets the input field and updates the tab order of pod controls.
514      * @param {boolean} takeFocus If true, input field takes focus.
515      */
516     reset: function(takeFocus) {
517       this.passwordElement.value = '';
518       if (takeFocus)
519         this.focusInput();  // This will set a custom tab order.
520       else
521         this.resetTabOrder();
522     },
524     /**
525      * Handles a click event on action area button.
526      * @param {Event} e Click event.
527      */
528     handleActionAreaButtonClick_: function(e) {
529       if (this.parentNode.disabled)
530         return;
531       this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
532     },
534     /**
535      * Handles a keydown event on action area button.
536      * @param {Event} e KeyDown event.
537      */
538     handleActionAreaButtonKeyDown_: function(e) {
539       if (this.disabled)
540         return;
541       switch (e.keyIdentifier) {
542         case 'Enter':
543         case 'U+0020':  // Space
544           if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
545             this.isActionBoxMenuActive = true;
546           e.stopPropagation();
547           break;
548         case 'Up':
549         case 'Down':
550           if (this.isActionBoxMenuActive) {
551             this.actionBoxMenuRemoveElement.tabIndex =
552                 UserPodTabOrder.PAD_MENU_ITEM;
553             this.actionBoxMenuRemoveElement.focus();
554           }
555           e.stopPropagation();
556           break;
557         case 'U+001B':  // Esc
558           this.isActionBoxMenuActive = false;
559           e.stopPropagation();
560           break;
561         case 'U+0009':  // Tab
562           this.parentNode.focusPod();
563         default:
564           this.isActionBoxMenuActive = false;
565           break;
566       }
567     },
569     /**
570      * Handles a click event on remove user command.
571      * @param {Event} e Click event.
572      */
573     handleRemoveCommandClick_: function(e) {
574       if (this.user.locallyManagedUser || this.user.isDesktopUser) {
575         this.showRemoveWarning_();
576         return;
577       }
578       if (this.isActionBoxMenuActive)
579         chrome.send('removeUser', [this.user.username]);
580     },
582     /**
583      * Shows remove warning for managed users.
584      */
585     showRemoveWarning_: function() {
586       this.actionBoxMenuRemoveElement.hidden = true;
587       this.actionBoxRemoveUserWarningElement.hidden = false;
588     },
590     /**
591      * Handles a click event on remove user confirmation button.
592      * @param {Event} e Click event.
593      */
594     handleRemoveUserConfirmationClick_: function(e) {
595       if (this.isActionBoxMenuActive)
596         chrome.send('removeUser', [this.user.username]);
597     },
599     /**
600      * Handles a keydown event on remove command.
601      * @param {Event} e KeyDown event.
602      */
603     handleRemoveCommandKeyDown_: function(e) {
604       if (this.disabled)
605         return;
606       switch (e.keyIdentifier) {
607         case 'Enter':
608           chrome.send('removeUser', [this.user.username]);
609           e.stopPropagation();
610           break;
611         case 'Up':
612         case 'Down':
613           e.stopPropagation();
614           break;
615         case 'U+001B':  // Esc
616           this.actionBoxAreaElement.focus();
617           this.isActionBoxMenuActive = false;
618           e.stopPropagation();
619           break;
620         default:
621           this.actionBoxAreaElement.focus();
622           this.isActionBoxMenuActive = false;
623           break;
624       }
625     },
627     /**
628      * Handles a blur event on remove command.
629      * @param {Event} e Blur event.
630      */
631     handleRemoveCommandBlur_: function(e) {
632       if (this.disabled)
633         return;
634       this.actionBoxMenuRemoveElement.tabIndex = -1;
635     },
637     /**
638      * Handles mousedown event on a user pod.
639      * @param {Event} e Mousedown event.
640      */
641     handleMouseDown_: function(e) {
642       if (this.parentNode.disabled)
643         return;
645       if (!this.signinButtonElement.hidden && !this.isActionBoxMenuActive) {
646         this.showSigninUI();
647         // Prevent default so that we don't trigger 'focus' event.
648         e.preventDefault();
649       }
650     }
651   };
653   /**
654    * Creates a public account user pod.
655    * @constructor
656    * @extends {UserPod}
657    */
658   var PublicAccountUserPod = cr.ui.define(function() {
659     var node = UserPod();
661     var extras = $('public-account-user-pod-extras-template').children;
662     for (var i = 0; i < extras.length; ++i) {
663       var el = extras[i].cloneNode(true);
664       node.appendChild(el);
665     }
667     return node;
668   });
670   PublicAccountUserPod.prototype = {
671     __proto__: UserPod.prototype,
673     /**
674      * "Enter" button in expanded side pane.
675      * @type {!HTMLButtonElement}
676      */
677     get enterButtonElement() {
678       return this.querySelector('.enter-button');
679     },
681     /**
682      * Boolean flag of whether the pod is showing the side pane. The flag
683      * controls whether 'expanded' class is added to the pod's class list and
684      * resets tab order because main input element changes when the 'expanded'
685      * state changes.
686      * @type {boolean}
687      */
688     get expanded() {
689       return this.classList.contains('expanded');
690     },
691     set expanded(expanded) {
692       if (this.expanded == expanded)
693         return;
695       this.resetTabOrder();
696       this.classList.toggle('expanded', expanded);
698       var self = this;
699       this.classList.add('animating');
700       this.addEventListener('webkitTransitionEnd', function f(e) {
701         self.removeEventListener('webkitTransitionEnd', f);
702         self.classList.remove('animating');
704         // Accessibility focus indicator does not move with the focused
705         // element. Sends a 'focus' event on the currently focused element
706         // so that accessibility focus indicator updates its location.
707         if (document.activeElement)
708           document.activeElement.dispatchEvent(new Event('focus'));
709       });
710     },
712     /** @override */
713     get needSignin() {
714       return false;
715     },
717     /** @override */
718     get mainInput() {
719       if (this.expanded)
720         return this.enterButtonElement;
721       else
722         return this.nameElement;
723     },
725     /** @override */
726     decorate: function() {
727       UserPod.prototype.decorate.call(this);
729       this.classList.remove('need-password');
730       this.classList.add('public-account');
732       this.nameElement.addEventListener('keydown', (function(e) {
733         if (e.keyIdentifier == 'Enter') {
734           this.parentNode.activatedPod = this;
735           // Stop this keydown event from bubbling up to PodRow handler.
736           e.stopPropagation();
737           // Prevent default so that we don't trigger a 'click' event on the
738           // newly focused "Enter" button.
739           e.preventDefault();
740         }
741       }).bind(this));
743       var learnMore = this.querySelector('.learn-more');
744       learnMore.addEventListener('mousedown', stopEventPropagation);
745       learnMore.addEventListener('click', this.handleLearnMoreEvent);
746       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
748       learnMore = this.querySelector('.side-pane-learn-more');
749       learnMore.addEventListener('click', this.handleLearnMoreEvent);
750       learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
752       this.enterButtonElement.addEventListener('click', (function(e) {
753         this.enterButtonElement.disabled = true;
754         chrome.send('launchPublicAccount', [this.user.username]);
755       }).bind(this));
756     },
758     /**
759      * Updates the user pod element.
760      */
761     update: function() {
762       UserPod.prototype.update.call(this);
763       this.querySelector('.side-pane-name').textContent =
764           this.user_.displayName;
765       this.querySelector('.info').textContent =
766           loadTimeData.getStringF('publicAccountInfoFormat',
767                                   this.user_.enterpriseDomain);
768     },
770     /** @override */
771     focusInput: function() {
772       // Move tabIndex from the whole pod to the main input.
773       this.tabIndex = -1;
774       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
775       this.mainInput.focus();
776     },
778     /** @override */
779     reset: function(takeFocus) {
780       if (!takeFocus)
781         this.expanded = false;
782       this.enterButtonElement.disabled = false;
783       UserPod.prototype.reset.call(this, takeFocus);
784     },
786     /** @override */
787     activate: function() {
788       this.expanded = true;
789       this.focusInput();
790       return true;
791     },
793     /** @override */
794     handleMouseDown_: function(e) {
795       if (this.parentNode.disabled)
796         return;
798       this.parentNode.focusPod(this);
799       this.parentNode.activatedPod = this;
800       // Prevent default so that we don't trigger 'focus' event.
801       e.preventDefault();
802     },
804     /**
805      * Handle mouse and keyboard events for the learn more button.
806      * Triggering the button causes information about public sessions to be
807      * shown.
808      * @param {Event} event Mouse or keyboard event.
809      */
810     handleLearnMoreEvent: function(event) {
811       switch (event.type) {
812         // Show informaton on left click. Let any other clicks propagate.
813         case 'click':
814           if (event.button != 0)
815             return;
816           break;
817         // Show informaton when <Return> or <Space> is pressed. Let any other
818         // key presses propagate.
819         case 'keydown':
820           switch (event.keyCode) {
821             case 13:  // Return.
822             case 32:  // Space.
823               break;
824             default:
825               return;
826           }
827           break;
828       }
829       chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
830       stopEventPropagation(event);
831     },
832   };
834   /**
835    * Creates a user pod to be used only in desktop chrome.
836    * @constructor
837    * @extends {UserPod}
838    */
839   var DesktopUserPod = cr.ui.define(function() {
840     // Don't just instantiate a UserPod(), as this will call decorate() on the
841     // parent object, and add duplicate event listeners.
842     var node = $('user-pod-template').cloneNode(true);
843     node.removeAttribute('id');
844     return node;
845   });
847   DesktopUserPod.prototype = {
848     __proto__: UserPod.prototype,
850     /** @override */
851     decorate: function() {
852       UserPod.prototype.decorate.call(this);
853     },
855     /** @override */
856     focusInput: function() {
857       var isLockedUser = this.user.needsSignin;
858       this.signinButtonElement.hidden = isLockedUser;
859       this.passwordElement.hidden = !isLockedUser;
861       // Move tabIndex from the whole pod to the main input.
862       this.tabIndex = -1;
863       this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
864       this.mainInput.focus();
865     },
867     /** @override */
868     update: function() {
869       // TODO(noms): Use the actual profile avatar for local profiles once the
870       // new, non-pixellated avatars are available.
871       this.imageElement.src = this.user.emailAddress == '' ?
872           'chrome://theme/IDR_USER_MANAGER_DEFAULT_AVATAR' :
873           this.user.userImage;
874       this.nameElement.textContent = this.user_.displayName;
875       var isLockedUser = this.user.needsSignin;
876       this.passwordElement.hidden = !isLockedUser;
877       this.signinButtonElement.hidden = isLockedUser;
879       UserPod.prototype.updateActionBoxArea.call(this);
880     },
882     /** @override */
883     activate: function() {
884       Oobe.launchUser(this.user.emailAddress, this.user.displayName);
885       return true;
886     },
888     /** @override */
889     handleMouseDown_: function(e) {
890       if (this.parentNode.disabled)
891         return;
893       // Don't sign in until the user presses the button. Just activate the pod.
894       Oobe.clearErrors();
895       this.parentNode.lastFocusedPod_ =
896           this.parentNode.getPodWithUsername_(this.user.emailAddress);
897     },
899     /** @override */
900     handleRemoveUserConfirmationClick_: function(e) {
901       chrome.send('removeUser', [this.user.profilePath]);
902     },
903   };
905   /**
906    * Creates a new pod row element.
907    * @constructor
908    * @extends {HTMLDivElement}
909    */
910   var PodRow = cr.ui.define('podrow');
912   PodRow.prototype = {
913     __proto__: HTMLDivElement.prototype,
915     // Whether this user pod row is shown for the first time.
916     firstShown_: true,
918     // Whether the initial wallpaper load after boot has been requested. Used
919     // only if |Oobe.getInstance().shouldLoadWallpaperOnBoot()| is true.
920     bootWallpaperLoaded_: false,
922     // True if inside focusPod().
923     insideFocusPod_: false,
925     // True if user pod has been activated with keyboard.
926     // In case of activation with keyboard we delay wallpaper change.
927     keyboardActivated_: false,
929     // Focused pod.
930     focusedPod_: undefined,
932     // Activated pod, i.e. the pod of current login attempt.
933     activatedPod_: undefined,
935     // Pod that was most recently focused, if any.
936     lastFocusedPod_: undefined,
938     // When moving through users quickly at login screen, set a timeout to
939     // prevent loading intermediate wallpapers.
940     loadWallpaperTimeout_: null,
942     // Pods whose initial images haven't been loaded yet.
943     podsWithPendingImages_: [],
945     /** @override */
946     decorate: function() {
947       this.style.left = 0;
949       // Event listeners that are installed for the time period during which
950       // the element is visible.
951       this.listeners_ = {
952         focus: [this.handleFocus_.bind(this), true],
953         click: [this.handleClick_.bind(this), false],
954         mousemove: [this.handleMouseMove_.bind(this), false],
955         keydown: [this.handleKeyDown.bind(this), false]
956       };
957     },
959     /**
960      * Returns all the pods in this pod row.
961      * @type {NodeList}
962      */
963     get pods() {
964       return this.children;
965     },
967     /**
968      * Return true if user pod row has only single user pod in it.
969      * @type {boolean}
970      */
971     get isSinglePod() {
972       return this.children.length == 1;
973     },
975     /**
976      * Returns pod with the given username (null if there is no such pod).
977      * @param {string} username Username to be matched.
978      * @return {Object} Pod with the given username. null if pod hasn't been
979      *                  found.
980      */
981     getPodWithUsername_: function(username) {
982       for (var i = 0, pod; pod = this.pods[i]; ++i) {
983         if (pod.user.username == username)
984           return pod;
985       }
986       return null;
987     },
989     /**
990      * True if the the pod row is disabled (handles no user interaction).
991      * @type {boolean}
992      */
993     disabled_: false,
994     get disabled() {
995       return this.disabled_;
996     },
997     set disabled(value) {
998       this.disabled_ = value;
999       var controls = this.querySelectorAll('button,input');
1000       for (var i = 0, control; control = controls[i]; ++i) {
1001         control.disabled = value;
1002       }
1003     },
1005     /**
1006      * Creates a user pod from given email.
1007      * @param {string} email User's email.
1008      */
1009     createUserPod: function(user) {
1010       var userPod;
1011       if (user.isDesktopUser)
1012         userPod = new DesktopUserPod({user: user});
1013       else if (user.publicAccount)
1014         userPod = new PublicAccountUserPod({user: user});
1015       else
1016         userPod = new UserPod({user: user});
1018       userPod.hidden = false;
1019       return userPod;
1020     },
1022     /**
1023      * Add an existing user pod to this pod row.
1024      * @param {!Object} user User info dictionary.
1025      * @param {boolean} animated Whether to use init animation.
1026      */
1027     addUserPod: function(user, animated) {
1028       var userPod = this.createUserPod(user);
1029       if (animated) {
1030         userPod.classList.add('init');
1031         userPod.nameElement.classList.add('init');
1032       }
1034       this.appendChild(userPod);
1035       userPod.initialize();
1036     },
1038     /**
1039      * Returns index of given pod or -1 if not found.
1040      * @param {UserPod} pod Pod to look up.
1041      * @private
1042      */
1043     indexOf_: function(pod) {
1044       for (var i = 0; i < this.pods.length; ++i) {
1045         if (pod == this.pods[i])
1046           return i;
1047       }
1048       return -1;
1049     },
1051     /**
1052      * Start first time show animation.
1053      */
1054     startInitAnimation: function() {
1055       // Schedule init animation.
1056       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1057         window.setTimeout(removeClass, 500 + i * 70, pod, 'init');
1058         window.setTimeout(removeClass, 700 + i * 70, pod.nameElement, 'init');
1059       }
1060     },
1062     /**
1063      * Start login success animation.
1064      */
1065     startAuthenticatedAnimation: function() {
1066       var activated = this.indexOf_(this.activatedPod_);
1067       if (activated == -1)
1068         return;
1070       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1071         if (i < activated)
1072           pod.classList.add('left');
1073         else if (i > activated)
1074           pod.classList.add('right');
1075         else
1076           pod.classList.add('zoom');
1077       }
1078     },
1080     /**
1081      * Populates pod row with given existing users and start init animation.
1082      * @param {array} users Array of existing user emails.
1083      * @param {boolean} animated Whether to use init animation.
1084      */
1085     loadPods: function(users, animated) {
1086       // Clear existing pods.
1087       this.innerHTML = '';
1088       this.focusedPod_ = undefined;
1089       this.activatedPod_ = undefined;
1090       this.lastFocusedPod_ = undefined;
1092       // Populate the pod row.
1093       for (var i = 0; i < users.length; ++i) {
1094         this.addUserPod(users[i], animated);
1095       }
1096       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1097         this.podsWithPendingImages_.push(pod);
1098       }
1099       // Make sure we eventually show the pod row, even if some image is stuck.
1100       setTimeout(function() {
1101         $('pod-row').classList.remove('images-loading');
1102       }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
1104       var columns = users.length < COLUMNS.length ?
1105           COLUMNS[users.length] : COLUMNS[COLUMNS.length - 1];
1106       var rows = Math.floor((users.length - 1) / columns) + 1;
1108       // Cancel any pending resize operation.
1109       this.removeEventListener('mouseout', this.deferredResizeListener_);
1111       if (!this.columns || !this.rows) {
1112         // Set initial dimensions.
1113         this.resize_(columns, rows);
1114       } else if (columns != this.columns || rows != this.rows) {
1115         // Defer the resize until mouse cursor leaves the pod row.
1116         this.deferredResizeListener_ = function(e) {
1117           if (!findAncestorByClass(e.toElement, 'podrow')) {
1118             this.resize_(columns, rows);
1119           }
1120         }.bind(this);
1121         this.addEventListener('mouseout', this.deferredResizeListener_);
1122       }
1124       this.focusPod(this.preselectedPod);
1125     },
1127     /**
1128      * Resizes the pod row and cancel any pending resize operations.
1129      * @param {number} columns Number of columns.
1130      * @param {number} rows Number of rows.
1131      * @private
1132      */
1133     resize_: function(columns, rows) {
1134       this.removeEventListener('mouseout', this.deferredResizeListener_);
1135       this.columns = columns;
1136       this.rows = rows;
1137       if (this.parentNode == Oobe.getInstance().currentScreen) {
1138         Oobe.getInstance().updateScreenSize(this.parentNode);
1139       }
1140     },
1142     /**
1143      * Number of columns.
1144      * @type {?number}
1145      */
1146     set columns(columns) {
1147       // Cannot use 'columns' here.
1148       this.setAttribute('ncolumns', columns);
1149     },
1150     get columns() {
1151       return this.getAttribute('ncolumns');
1152     },
1154     /**
1155      * Number of rows.
1156      * @type {?number}
1157      */
1158     set rows(rows) {
1159       // Cannot use 'rows' here.
1160       this.setAttribute('nrows', rows);
1161     },
1162     get rows() {
1163       return this.getAttribute('nrows');
1164     },
1166     /**
1167      * Whether the pod is currently focused.
1168      * @param {UserPod} pod Pod to check for focus.
1169      * @return {boolean} Pod focus status.
1170      */
1171     isFocused: function(pod) {
1172       return this.focusedPod_ == pod;
1173     },
1175     /**
1176      * Focuses a given user pod or clear focus when given null.
1177      * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
1178      * @param {boolean=} opt_force If true, forces focus update even when
1179      *                             podToFocus is already focused.
1180      */
1181     focusPod: function(podToFocus, opt_force) {
1182       if (this.isFocused(podToFocus) && !opt_force) {
1183         this.keyboardActivated_ = false;
1184         return;
1185       }
1187       // Make sure there's only one focusPod operation happening at a time.
1188       if (this.insideFocusPod_) {
1189         this.keyboardActivated_ = false;
1190         return;
1191       }
1192       this.insideFocusPod_ = true;
1194       clearTimeout(this.loadWallpaperTimeout_);
1195       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1196         if (!this.isSinglePod) {
1197           pod.isActionBoxMenuActive = false;
1198         }
1199         if (pod != podToFocus) {
1200           pod.isActionBoxMenuHovered = false;
1201           pod.classList.remove('focused');
1202           pod.classList.remove('faded');
1203           pod.reset(false);
1204         }
1205       }
1207       // Clear any error messages for previous pod.
1208       if (!this.isFocused(podToFocus))
1209         Oobe.clearErrors();
1211       var hadFocus = !!this.focusedPod_;
1212       this.focusedPod_ = podToFocus;
1213       if (podToFocus) {
1214         podToFocus.classList.remove('faded');
1215         podToFocus.classList.add('focused');
1216         podToFocus.reset(true);  // Reset and give focus.
1217         chrome.send('focusPod', [podToFocus.user.emailAddress]);
1218         if (hadFocus && this.keyboardActivated_) {
1219           // Delay wallpaper loading to let user tab through pods without lag.
1220           this.loadWallpaperTimeout_ = window.setTimeout(
1221               this.loadWallpaper_.bind(this), WALLPAPER_LOAD_DELAY_MS);
1222         } else if (!this.firstShown_) {
1223           // Load wallpaper immediately if there no pod was focused
1224           // previously, and it is not a boot into user pod list case.
1225           this.loadWallpaper_();
1226         }
1227         this.firstShown_ = false;
1228         this.lastFocusedPod_ = podToFocus;
1229       }
1230       this.insideFocusPod_ = false;
1231       this.keyboardActivated_ = false;
1232     },
1234     /**
1235      * Loads wallpaper for the active user pod, if any.
1236      * @private
1237      */
1238     loadWallpaper_: function() {
1239       if (this.focusedPod_)
1240         chrome.send('loadWallpaper', [this.focusedPod_.user.username]);
1241     },
1243     /**
1244      * Resets wallpaper to the last active user's wallpaper, if any.
1245      */
1246     loadLastWallpaper: function() {
1247       if (this.lastFocusedPod_)
1248         chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
1249     },
1251     /**
1252      * Returns the currently activated pod.
1253      * @type {UserPod}
1254      */
1255     get activatedPod() {
1256       return this.activatedPod_;
1257     },
1258     set activatedPod(pod) {
1259       if (pod && pod.activate())
1260         this.activatedPod_ = pod;
1261     },
1263     /**
1264      * The pod of the signed-in user, if any; null otherwise.
1265      * @type {?UserPod}
1266      */
1267     get lockedPod() {
1268       for (var i = 0, pod; pod = this.pods[i]; ++i) {
1269         if (pod.user.signedIn)
1270           return pod;
1271       }
1272       return null;
1273     },
1275     /**
1276      * The pod that is preselected on user pod row show.
1277      * @type {?UserPod}
1278      */
1279     get preselectedPod() {
1280       var lockedPod = this.lockedPod;
1281       var preselectedPod = PRESELECT_FIRST_POD ?
1282           lockedPod || this.pods[0] : lockedPod;
1283       return preselectedPod;
1284     },
1286     /**
1287      * Resets input UI.
1288      * @param {boolean} takeFocus True to take focus.
1289      */
1290     reset: function(takeFocus) {
1291       this.disabled = false;
1292       if (this.activatedPod_)
1293         this.activatedPod_.reset(takeFocus);
1294     },
1296     /**
1297      * Restores input focus to current selected pod, if there is any.
1298      */
1299     refocusCurrentPod: function() {
1300       if (this.focusedPod_) {
1301         this.focusedPod_.focusInput();
1302       }
1303     },
1305     /**
1306      * Clears focused pod password field.
1307      */
1308     clearFocusedPod: function() {
1309       if (!this.disabled && this.focusedPod_)
1310         this.focusedPod_.reset(true);
1311     },
1313     /**
1314      * Shows signin UI.
1315      * @param {string} email Email for signin UI.
1316      */
1317     showSigninUI: function(email) {
1318       // Clear any error messages that might still be around.
1319       Oobe.clearErrors();
1320       this.disabled = true;
1321       this.lastFocusedPod_ = this.getPodWithUsername_(email);
1322       Oobe.showSigninUI(email);
1323     },
1325     /**
1326      * Updates current image of a user.
1327      * @param {string} username User for which to update the image.
1328      */
1329     updateUserImage: function(username) {
1330       var pod = this.getPodWithUsername_(username);
1331       if (pod)
1332         pod.updateUserImage();
1333     },
1335     /**
1336      * Resets OAuth token status (invalidates it).
1337      * @param {string} username User for which to reset the status.
1338      */
1339     resetUserOAuthTokenStatus: function(username) {
1340       var pod = this.getPodWithUsername_(username);
1341       if (pod) {
1342         pod.user.oauthTokenStatus = OAuthTokenStatus.INVALID_OLD;
1343         pod.update();
1344       } else {
1345         console.log('Failed to update Gaia state for: ' + username);
1346       }
1347     },
1349     /**
1350      * Handler of click event.
1351      * @param {Event} e Click Event object.
1352      * @private
1353      */
1354     handleClick_: function(e) {
1355       if (this.disabled)
1356         return;
1358       // Clear all menus if the click is outside pod menu and its
1359       // button area.
1360       if (!findAncestorByClass(e.target, 'action-box-menu') &&
1361           !findAncestorByClass(e.target, 'action-box-area')) {
1362         for (var i = 0, pod; pod = this.pods[i]; ++i)
1363           pod.isActionBoxMenuActive = false;
1364       }
1366       // Clears focus if not clicked on a pod and if there's more than one pod.
1367       var pod = findAncestorByClass(e.target, 'pod');
1368       if ((!pod || pod.parentNode != this) && !this.isSinglePod) {
1369         this.focusPod();
1370       }
1372       if (pod)
1373         pod.isActionBoxMenuHovered = true;
1375       // Return focus back to single pod.
1376       if (this.isSinglePod) {
1377         this.focusPod(this.focusedPod_, true /* force */);
1378         if (!pod)
1379           this.focusedPod_.isActionBoxMenuHovered = false;
1380       }
1381     },
1383     /**
1384      * Handler of mouse move event.
1385      * @param {Event} e Click Event object.
1386      * @private
1387      */
1388     handleMouseMove_: function(e) {
1389       if (this.disabled)
1390         return;
1391       if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
1392         return;
1394       // Defocus (thus hide) action box, if it is focused on a user pod
1395       // and the pointer is not hovering over it.
1396       var pod = findAncestorByClass(e.target, 'pod');
1397       if (document.activeElement &&
1398           document.activeElement.parentNode != pod &&
1399           document.activeElement.classList.contains('action-box-area')) {
1400         document.activeElement.parentNode.focus();
1401       }
1403       if (pod)
1404         pod.isActionBoxMenuHovered = true;
1406       // Hide action boxes on other user pods.
1407       for (var i = 0, p; p = this.pods[i]; ++i)
1408         if (p != pod && !p.isActionBoxMenuActive)
1409           p.isActionBoxMenuHovered = false;
1410     },
1412     /**
1413      * Handles focus event.
1414      * @param {Event} e Focus Event object.
1415      * @private
1416      */
1417     handleFocus_: function(e) {
1418       if (this.disabled)
1419         return;
1420       if (e.target.parentNode == this) {
1421         // Focus on a pod
1422         if (e.target.classList.contains('focused'))
1423           e.target.focusInput();
1424         else
1425           this.focusPod(e.target);
1426         return;
1427       }
1429       var pod = findAncestorByClass(e.target, 'pod');
1430       if (pod && pod.parentNode == this) {
1431         // Focus on a control of a pod but not on the action area button.
1432         if (!pod.classList.contains('focused') &&
1433             !e.target.classList.contains('action-box-button')) {
1434           this.focusPod(pod);
1435           e.target.focus();
1436         }
1437         return;
1438       }
1440       // Clears pod focus when we reach here. It means new focus is neither
1441       // on a pod nor on a button/input for a pod.
1442       // Do not "defocus" user pod when it is a single pod.
1443       // That means that 'focused' class will not be removed and
1444       // input field/button will always be visible.
1445       if (!this.isSinglePod)
1446         this.focusPod();
1447     },
1449     /**
1450      * Handler of keydown event.
1451      * @param {Event} e KeyDown Event object.
1452      */
1453     handleKeyDown: function(e) {
1454       if (this.disabled)
1455         return;
1456       var editing = e.target.tagName == 'INPUT' && e.target.value;
1457       switch (e.keyIdentifier) {
1458         case 'Left':
1459           if (!editing) {
1460             this.keyboardActivated_ = true;
1461             if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
1462               this.focusPod(this.focusedPod_.previousElementSibling);
1463             else
1464               this.focusPod(this.lastElementChild);
1466             e.stopPropagation();
1467           }
1468           break;
1469         case 'Right':
1470           if (!editing) {
1471             this.keyboardActivated_ = true;
1472             if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
1473               this.focusPod(this.focusedPod_.nextElementSibling);
1474             else
1475               this.focusPod(this.firstElementChild);
1477             e.stopPropagation();
1478           }
1479           break;
1480         case 'Enter':
1481           if (this.focusedPod_) {
1482             this.activatedPod = this.focusedPod_;
1483             e.stopPropagation();
1484           }
1485           break;
1486         case 'U+001B':  // Esc
1487           if (!this.isSinglePod)
1488             this.focusPod();
1489           break;
1490       }
1491     },
1493     /**
1494      * Called right after the pod row is shown.
1495      */
1496     handleAfterShow: function() {
1497       // Force input focus for user pod on show and once transition ends.
1498       if (this.focusedPod_) {
1499         var focusedPod = this.focusedPod_;
1500         var screen = this.parentNode;
1501         var self = this;
1502         focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
1503           if (e.target == focusedPod) {
1504             focusedPod.removeEventListener('webkitTransitionEnd', f);
1505             focusedPod.reset(true);
1506             // Notify screen that it is ready.
1507             screen.onShow();
1508             // Boot transition: load wallpaper.
1509             if (!self.bootWallpaperLoaded_ &&
1510                 Oobe.getInstance().shouldLoadWallpaperOnBoot()) {
1511               self.loadWallpaperTimeout_ = window.setTimeout(
1512                   self.loadWallpaper_.bind(self), WALLPAPER_BOOT_LOAD_DELAY_MS);
1513               self.bootWallpaperLoaded_ = true;
1514             }
1515           }
1516         });
1517       }
1518     },
1520     /**
1521      * Called right before the pod row is shown.
1522      */
1523     handleBeforeShow: function() {
1524       for (var event in this.listeners_) {
1525         this.ownerDocument.addEventListener(
1526             event, this.listeners_[event][0], this.listeners_[event][1]);
1527       }
1528       $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
1529     },
1531     /**
1532      * Called when the element is hidden.
1533      */
1534     handleHide: function() {
1535       for (var event in this.listeners_) {
1536         this.ownerDocument.removeEventListener(
1537             event, this.listeners_[event][0], this.listeners_[event][1]);
1538       }
1539       $('login-header-bar').buttonsTabIndex = 0;
1540     },
1542     /**
1543      * Called when a pod's user image finishes loading.
1544      */
1545     handlePodImageLoad: function(pod) {
1546       var index = this.podsWithPendingImages_.indexOf(pod);
1547       if (index == -1) {
1548         return;
1549       }
1551       this.podsWithPendingImages_.splice(index, 1);
1552       if (this.podsWithPendingImages_.length == 0) {
1553         this.classList.remove('images-loading');
1554       }
1555     }
1556   };
1558   return {
1559     PodRow: PodRow
1560   };