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.
6 * @fileoverview User pod row implementation.
9 cr.define('login', function() {
11 * Number of displayed columns depending on user pod count.
12 * @type {Array.<number>}
15 var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6];
18 * Whether to preselect the first pod automatically on login screen.
22 var PRESELECT_FIRST_POD = true;
25 * Wallpaper load delay in milliseconds.
29 var WALLPAPER_LOAD_DELAY_MS = 500;
32 * Wallpaper load delay in milliseconds. TODO(nkostylev): Tune this constant.
36 var WALLPAPER_BOOT_LOAD_DELAY_MS = 100;
39 * Maximum time for which the pod row remains hidden until all user images
44 var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
47 * Public session help topic identifier.
51 var HELP_TOPIC_PUBLIC_SESSION = 3041033;
54 * Oauth token status. These must match UserManager::OAuthTokenStatus.
58 var OAuthTokenStatus = {
67 * Tab order for user pods. Update these when adding new controls.
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).
78 // Focus and tab order are organized as follows:
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.
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.
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.
96 function removeClass(el, cl) {
97 el.classList.remove(cl);
101 * Creates a user pod.
103 * @extends {HTMLDivElement}
105 var UserPod = cr.ui.define(function() {
106 var node = $('user-pod-template').cloneNode(true);
107 node.removeAttribute('id');
112 * Stops event propagation from the any user pod child element.
113 * @param {Event} e Event to handle.
115 function stopEventPropagation(e) {
116 // Prevent default so that we don't trigger a 'focus' event.
122 * Unique salt added to user image URLs to prevent caching. Dictionary with
123 * user names as keys.
126 UserPod.userImageSalt_ = {};
128 UserPod.prototype = {
129 __proto__: HTMLDivElement.prototype,
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'
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(
161 this.handleRemoveUserConfirmationClick_.bind(this));
166 * Initializes the pod after its properties set and added to a pod row.
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));
179 * Resets tab order for pod elements to its initial state.
181 resetTabOrder: function() {
182 this.tabIndex = UserPodTabOrder.POD_INPUT;
183 this.mainInput.tabIndex = -1;
187 * Handles keypress event (i.e. any textual input) on password input.
188 * @param {Event} e Keypress Event object.
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) {
201 * Gets signed in indicator element.
202 * @type {!HTMLDivElement}
204 get signedInIndicatorElement() {
205 return this.querySelector('.signed-in-indicator');
209 * Gets image element.
210 * @type {!HTMLImageElement}
213 return this.querySelector('.user-image');
218 * @type {!HTMLDivElement}
221 return this.querySelector('.name');
225 * Gets password field.
226 * @type {!HTMLInputElement}
228 get passwordElement() {
229 return this.querySelector('.password');
233 * Gets Caps Lock hint image.
234 * @type {!HTMLImageElement}
236 get capslockHintElement() {
237 return this.querySelector('.capslock-hint');
241 * Gets user signin button.
242 * @type {!HTMLInputElement}
244 get signinButtonElement() {
245 return this.querySelector('.signin-button');
249 * Gets action box area.
250 * @type {!HTMLInputElement}
252 get actionBoxAreaElement() {
253 return this.querySelector('.action-box-area');
257 * Gets user type icon area.
258 * @type {!HTMLInputElement}
260 get userTypeIconAreaElement() {
261 return this.querySelector('.user-type-icon-area');
265 * Gets action box menu.
266 * @type {!HTMLInputElement}
268 get actionBoxMenuElement() {
269 return this.querySelector('.action-box-menu');
273 * Gets action box menu title.
274 * @type {!HTMLInputElement}
276 get actionBoxMenuTitleElement() {
277 return this.querySelector('.action-box-menu-title');
281 * Gets action box menu title, user name item.
282 * @type {!HTMLInputElement}
284 get actionBoxMenuTitleNameElement() {
285 return this.querySelector('.action-box-menu-title-name');
289 * Gets action box menu title, user email item.
290 * @type {!HTMLInputElement}
292 get actionBoxMenuTitleEmailElement() {
293 return this.querySelector('.action-box-menu-title-email');
297 * Gets action box menu, remove user command item.
298 * @type {!HTMLInputElement}
300 get actionBoxMenuCommandElement() {
301 return this.querySelector('.action-box-menu-remove-command');
305 * Gets action box menu, remove user command item div.
306 * @type {!HTMLInputElement}
308 get actionBoxMenuRemoveElement() {
309 return this.querySelector('.action-box-menu-remove');
313 * Gets action box menu, remove user command item div.
314 * @type {!HTMLInputElement}
316 get actionBoxRemoveUserWarningElement() {
317 return this.querySelector('.action-box-remove-user-warning');
321 * Gets action box menu, remove user command item div.
322 * @type {!HTMLInputElement}
324 get actionBoxRemoveUserWarningButtonElement() {
325 return this.querySelector(
326 '.remove-warning-button');
330 * Updates the user pod element.
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();
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;
371 * The user that this pod represents.
379 this.user_ = userDict;
384 * Whether signin is required for this user.
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 &&
395 * Gets main input element.
396 * @type {(HTMLButtonElement|HTMLInputElement)}
399 if (!this.signinButtonElement.hidden)
400 return this.signinButtonElement;
402 return this.passwordElement;
406 * Whether action box button is in active state.
409 get isActionBoxMenuActive() {
410 return this.actionBoxAreaElement.classList.contains('active');
412 set isActionBoxMenuActive(active) {
413 if (active == this.isActionBoxMenuActive)
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();
426 this.actionBoxAreaElement.classList.add('active');
428 this.actionBoxAreaElement.classList.remove('active');
433 * Whether action box button is in hovered state.
436 get isActionBoxMenuHovered() {
437 return this.actionBoxAreaElement.classList.contains('hovered');
439 set isActionBoxMenuHovered(hovered) {
440 if (hovered == this.isActionBoxMenuHovered)
444 this.actionBoxAreaElement.classList.add('hovered');
446 this.actionBoxAreaElement.classList.remove('hovered');
451 * Updates the image element of the user.
453 updateUserImage: function() {
454 UserPod.userImageSalt_[this.user.username] = new Date().getTime();
459 * Focuses on input element.
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.
468 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
469 this.mainInput.focus();
474 * @return {boolean} True if activated successfully.
476 activate: function() {
477 if (!this.signinButtonElement.hidden) {
479 } else if (!this.passwordElement.value) {
482 Oobe.disableSigninUI();
483 chrome.send('authenticateUser',
484 [this.user.username, this.passwordElement.value]);
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,
502 * Shows signin UI for this user.
504 showSigninUI: function() {
505 if (this.user.locallyManagedUser) {
506 this.showSupervisedUserSigninWarning();
508 this.parentNode.showSigninUI(this.user.emailAddress);
513 * Resets the input field and updates the tab order of pod controls.
514 * @param {boolean} takeFocus If true, input field takes focus.
516 reset: function(takeFocus) {
517 this.passwordElement.value = '';
519 this.focusInput(); // This will set a custom tab order.
521 this.resetTabOrder();
525 * Handles a click event on action area button.
526 * @param {Event} e Click event.
528 handleActionAreaButtonClick_: function(e) {
529 if (this.parentNode.disabled)
531 this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
535 * Handles a keydown event on action area button.
536 * @param {Event} e KeyDown event.
538 handleActionAreaButtonKeyDown_: function(e) {
541 switch (e.keyIdentifier) {
543 case 'U+0020': // Space
544 if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
545 this.isActionBoxMenuActive = true;
550 if (this.isActionBoxMenuActive) {
551 this.actionBoxMenuRemoveElement.tabIndex =
552 UserPodTabOrder.PAD_MENU_ITEM;
553 this.actionBoxMenuRemoveElement.focus();
557 case 'U+001B': // Esc
558 this.isActionBoxMenuActive = false;
561 case 'U+0009': // Tab
562 this.parentNode.focusPod();
564 this.isActionBoxMenuActive = false;
570 * Handles a click event on remove user command.
571 * @param {Event} e Click event.
573 handleRemoveCommandClick_: function(e) {
574 if (this.user.locallyManagedUser || this.user.isDesktopUser) {
575 this.showRemoveWarning_();
578 if (this.isActionBoxMenuActive)
579 chrome.send('removeUser', [this.user.username]);
583 * Shows remove warning for managed users.
585 showRemoveWarning_: function() {
586 this.actionBoxMenuRemoveElement.hidden = true;
587 this.actionBoxRemoveUserWarningElement.hidden = false;
591 * Handles a click event on remove user confirmation button.
592 * @param {Event} e Click event.
594 handleRemoveUserConfirmationClick_: function(e) {
595 if (this.isActionBoxMenuActive)
596 chrome.send('removeUser', [this.user.username]);
600 * Handles a keydown event on remove command.
601 * @param {Event} e KeyDown event.
603 handleRemoveCommandKeyDown_: function(e) {
606 switch (e.keyIdentifier) {
608 chrome.send('removeUser', [this.user.username]);
615 case 'U+001B': // Esc
616 this.actionBoxAreaElement.focus();
617 this.isActionBoxMenuActive = false;
621 this.actionBoxAreaElement.focus();
622 this.isActionBoxMenuActive = false;
628 * Handles a blur event on remove command.
629 * @param {Event} e Blur event.
631 handleRemoveCommandBlur_: function(e) {
634 this.actionBoxMenuRemoveElement.tabIndex = -1;
638 * Handles mousedown event on a user pod.
639 * @param {Event} e Mousedown event.
641 handleMouseDown_: function(e) {
642 if (this.parentNode.disabled)
645 if (!this.signinButtonElement.hidden && !this.isActionBoxMenuActive) {
647 // Prevent default so that we don't trigger 'focus' event.
654 * Creates a public account user pod.
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);
670 PublicAccountUserPod.prototype = {
671 __proto__: UserPod.prototype,
674 * "Enter" button in expanded side pane.
675 * @type {!HTMLButtonElement}
677 get enterButtonElement() {
678 return this.querySelector('.enter-button');
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'
689 return this.classList.contains('expanded');
691 set expanded(expanded) {
692 if (this.expanded == expanded)
695 this.resetTabOrder();
696 this.classList.toggle('expanded', expanded);
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'));
720 return this.enterButtonElement;
722 return this.nameElement;
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.
737 // Prevent default so that we don't trigger a 'click' event on the
738 // newly focused "Enter" button.
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]);
759 * Updates the user pod element.
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);
771 focusInput: function() {
772 // Move tabIndex from the whole pod to the main input.
774 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
775 this.mainInput.focus();
779 reset: function(takeFocus) {
781 this.expanded = false;
782 this.enterButtonElement.disabled = false;
783 UserPod.prototype.reset.call(this, takeFocus);
787 activate: function() {
788 this.expanded = true;
794 handleMouseDown_: function(e) {
795 if (this.parentNode.disabled)
798 this.parentNode.focusPod(this);
799 this.parentNode.activatedPod = this;
800 // Prevent default so that we don't trigger 'focus' event.
805 * Handle mouse and keyboard events for the learn more button.
806 * Triggering the button causes information about public sessions to be
808 * @param {Event} event Mouse or keyboard event.
810 handleLearnMoreEvent: function(event) {
811 switch (event.type) {
812 // Show informaton on left click. Let any other clicks propagate.
814 if (event.button != 0)
817 // Show informaton when <Return> or <Space> is pressed. Let any other
818 // key presses propagate.
820 switch (event.keyCode) {
829 chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
830 stopEventPropagation(event);
835 * Creates a user pod to be used only in desktop chrome.
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');
847 DesktopUserPod.prototype = {
848 __proto__: UserPod.prototype,
851 decorate: function() {
852 UserPod.prototype.decorate.call(this);
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.
863 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
864 this.mainInput.focus();
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' :
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);
883 activate: function() {
884 Oobe.launchUser(this.user.emailAddress, this.user.displayName);
889 handleMouseDown_: function(e) {
890 if (this.parentNode.disabled)
893 // Don't sign in until the user presses the button. Just activate the pod.
895 this.parentNode.lastFocusedPod_ =
896 this.parentNode.getPodWithUsername_(this.user.emailAddress);
900 handleRemoveUserConfirmationClick_: function(e) {
901 chrome.send('removeUser', [this.user.profilePath]);
906 * Creates a new pod row element.
908 * @extends {HTMLDivElement}
910 var PodRow = cr.ui.define('podrow');
913 __proto__: HTMLDivElement.prototype,
915 // Whether this user pod row is shown for the first time.
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,
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_: [],
946 decorate: function() {
949 // Event listeners that are installed for the time period during which
950 // the element is visible.
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]
960 * Returns all the pods in this pod row.
964 return this.children;
968 * Return true if user pod row has only single user pod in it.
972 return this.children.length == 1;
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
981 getPodWithUsername_: function(username) {
982 for (var i = 0, pod; pod = this.pods[i]; ++i) {
983 if (pod.user.username == username)
990 * True if the the pod row is disabled (handles no user interaction).
995 return this.disabled_;
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;
1006 * Creates a user pod from given email.
1007 * @param {string} email User's email.
1009 createUserPod: function(user) {
1011 if (user.isDesktopUser)
1012 userPod = new DesktopUserPod({user: user});
1013 else if (user.publicAccount)
1014 userPod = new PublicAccountUserPod({user: user});
1016 userPod = new UserPod({user: user});
1018 userPod.hidden = false;
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.
1027 addUserPod: function(user, animated) {
1028 var userPod = this.createUserPod(user);
1030 userPod.classList.add('init');
1031 userPod.nameElement.classList.add('init');
1034 this.appendChild(userPod);
1035 userPod.initialize();
1039 * Returns index of given pod or -1 if not found.
1040 * @param {UserPod} pod Pod to look up.
1043 indexOf_: function(pod) {
1044 for (var i = 0; i < this.pods.length; ++i) {
1045 if (pod == this.pods[i])
1052 * Start first time show animation.
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');
1063 * Start login success animation.
1065 startAuthenticatedAnimation: function() {
1066 var activated = this.indexOf_(this.activatedPod_);
1067 if (activated == -1)
1070 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1072 pod.classList.add('left');
1073 else if (i > activated)
1074 pod.classList.add('right');
1076 pod.classList.add('zoom');
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.
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);
1096 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1097 this.podsWithPendingImages_.push(pod);
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);
1121 this.addEventListener('mouseout', this.deferredResizeListener_);
1124 this.focusPod(this.preselectedPod);
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.
1133 resize_: function(columns, rows) {
1134 this.removeEventListener('mouseout', this.deferredResizeListener_);
1135 this.columns = columns;
1137 if (this.parentNode == Oobe.getInstance().currentScreen) {
1138 Oobe.getInstance().updateScreenSize(this.parentNode);
1143 * Number of columns.
1146 set columns(columns) {
1147 // Cannot use 'columns' here.
1148 this.setAttribute('ncolumns', columns);
1151 return this.getAttribute('ncolumns');
1159 // Cannot use 'rows' here.
1160 this.setAttribute('nrows', rows);
1163 return this.getAttribute('nrows');
1167 * Whether the pod is currently focused.
1168 * @param {UserPod} pod Pod to check for focus.
1169 * @return {boolean} Pod focus status.
1171 isFocused: function(pod) {
1172 return this.focusedPod_ == pod;
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.
1181 focusPod: function(podToFocus, opt_force) {
1182 if (this.isFocused(podToFocus) && !opt_force) {
1183 this.keyboardActivated_ = false;
1187 // Make sure there's only one focusPod operation happening at a time.
1188 if (this.insideFocusPod_) {
1189 this.keyboardActivated_ = false;
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;
1199 if (pod != podToFocus) {
1200 pod.isActionBoxMenuHovered = false;
1201 pod.classList.remove('focused');
1202 pod.classList.remove('faded');
1207 // Clear any error messages for previous pod.
1208 if (!this.isFocused(podToFocus))
1211 var hadFocus = !!this.focusedPod_;
1212 this.focusedPod_ = 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_();
1227 this.firstShown_ = false;
1228 this.lastFocusedPod_ = podToFocus;
1230 this.insideFocusPod_ = false;
1231 this.keyboardActivated_ = false;
1235 * Loads wallpaper for the active user pod, if any.
1238 loadWallpaper_: function() {
1239 if (this.focusedPod_)
1240 chrome.send('loadWallpaper', [this.focusedPod_.user.username]);
1244 * Resets wallpaper to the last active user's wallpaper, if any.
1246 loadLastWallpaper: function() {
1247 if (this.lastFocusedPod_)
1248 chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
1252 * Returns the currently activated pod.
1255 get activatedPod() {
1256 return this.activatedPod_;
1258 set activatedPod(pod) {
1259 if (pod && pod.activate())
1260 this.activatedPod_ = pod;
1264 * The pod of the signed-in user, if any; null otherwise.
1268 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1269 if (pod.user.signedIn)
1276 * The pod that is preselected on user pod row show.
1279 get preselectedPod() {
1280 var lockedPod = this.lockedPod;
1281 var preselectedPod = PRESELECT_FIRST_POD ?
1282 lockedPod || this.pods[0] : lockedPod;
1283 return preselectedPod;
1288 * @param {boolean} takeFocus True to take focus.
1290 reset: function(takeFocus) {
1291 this.disabled = false;
1292 if (this.activatedPod_)
1293 this.activatedPod_.reset(takeFocus);
1297 * Restores input focus to current selected pod, if there is any.
1299 refocusCurrentPod: function() {
1300 if (this.focusedPod_) {
1301 this.focusedPod_.focusInput();
1306 * Clears focused pod password field.
1308 clearFocusedPod: function() {
1309 if (!this.disabled && this.focusedPod_)
1310 this.focusedPod_.reset(true);
1315 * @param {string} email Email for signin UI.
1317 showSigninUI: function(email) {
1318 // Clear any error messages that might still be around.
1320 this.disabled = true;
1321 this.lastFocusedPod_ = this.getPodWithUsername_(email);
1322 Oobe.showSigninUI(email);
1326 * Updates current image of a user.
1327 * @param {string} username User for which to update the image.
1329 updateUserImage: function(username) {
1330 var pod = this.getPodWithUsername_(username);
1332 pod.updateUserImage();
1336 * Resets OAuth token status (invalidates it).
1337 * @param {string} username User for which to reset the status.
1339 resetUserOAuthTokenStatus: function(username) {
1340 var pod = this.getPodWithUsername_(username);
1342 pod.user.oauthTokenStatus = OAuthTokenStatus.INVALID_OLD;
1345 console.log('Failed to update Gaia state for: ' + username);
1350 * Handler of click event.
1351 * @param {Event} e Click Event object.
1354 handleClick_: function(e) {
1358 // Clear all menus if the click is outside pod menu and its
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;
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) {
1373 pod.isActionBoxMenuHovered = true;
1375 // Return focus back to single pod.
1376 if (this.isSinglePod) {
1377 this.focusPod(this.focusedPod_, true /* force */);
1379 this.focusedPod_.isActionBoxMenuHovered = false;
1384 * Handler of mouse move event.
1385 * @param {Event} e Click Event object.
1388 handleMouseMove_: function(e) {
1391 if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
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();
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;
1413 * Handles focus event.
1414 * @param {Event} e Focus Event object.
1417 handleFocus_: function(e) {
1420 if (e.target.parentNode == this) {
1422 if (e.target.classList.contains('focused'))
1423 e.target.focusInput();
1425 this.focusPod(e.target);
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')) {
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)
1450 * Handler of keydown event.
1451 * @param {Event} e KeyDown Event object.
1453 handleKeyDown: function(e) {
1456 var editing = e.target.tagName == 'INPUT' && e.target.value;
1457 switch (e.keyIdentifier) {
1460 this.keyboardActivated_ = true;
1461 if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
1462 this.focusPod(this.focusedPod_.previousElementSibling);
1464 this.focusPod(this.lastElementChild);
1466 e.stopPropagation();
1471 this.keyboardActivated_ = true;
1472 if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
1473 this.focusPod(this.focusedPod_.nextElementSibling);
1475 this.focusPod(this.firstElementChild);
1477 e.stopPropagation();
1481 if (this.focusedPod_) {
1482 this.activatedPod = this.focusedPod_;
1483 e.stopPropagation();
1486 case 'U+001B': // Esc
1487 if (!this.isSinglePod)
1494 * Called right after the pod row is shown.
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;
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.
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;
1521 * Called right before the pod row is shown.
1523 handleBeforeShow: function() {
1524 for (var event in this.listeners_) {
1525 this.ownerDocument.addEventListener(
1526 event, this.listeners_[event][0], this.listeners_[event][1]);
1528 $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
1532 * Called when the element is hidden.
1534 handleHide: function() {
1535 for (var event in this.listeners_) {
1536 this.ownerDocument.removeEventListener(
1537 event, this.listeners_[event][0], this.listeners_[event][1]);
1539 $('login-header-bar').buttonsTabIndex = 0;
1543 * Called when a pod's user image finishes loading.
1545 handlePodImageLoad: function(pod) {
1546 var index = this.podsWithPendingImages_.indexOf(pod);
1551 this.podsWithPendingImages_.splice(index, 1);
1552 if (this.podsWithPendingImages_.length == 0) {
1553 this.classList.remove('images-loading');