1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
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 * Mapping between number of columns in pod-row and margin between user pods
20 * @type {Array.<number>}
23 var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12];
26 * Maximal number of columns currently supported by pod-row.
30 var MAX_NUMBER_OF_COLUMNS = 6;
33 * Maximal number of rows if sign-in banner is displayed alonside.
37 var MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER = 2;
40 * Variables used for pod placement processing. Width and height should be
41 * synced with computed CSS sizes of pods.
44 var PUBLIC_EXPANDED_WIDTH = 420;
45 var CROS_POD_HEIGHT = 213;
46 var DESKTOP_POD_HEIGHT = 216;
47 var POD_ROW_PADDING = 10;
50 * Whether to preselect the first pod automatically on login screen.
54 var PRESELECT_FIRST_POD = true;
57 * Maximum time for which the pod row remains hidden until all user images
62 var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
65 * Public session help topic identifier.
69 var HELP_TOPIC_PUBLIC_SESSION = 3041033;
72 * Tab order for user pods. Update these when adding new controls.
76 var UserPodTabOrder = {
77 POD_INPUT: 1, // Password input fields (and whole pods themselves).
78 HEADER_BAR: 2, // Buttons on the header bar (Shutdown, Add User).
79 ACTION_BOX: 3, // Action box buttons.
80 PAD_MENU_ITEM: 4 // User pad menu items (Remove this user).
84 * Supported authentication types. Keep in sync with the enum in
85 * chrome/browser/chromeos/login/login_display.h
97 * Names of authentication types.
99 var AUTH_TYPE_NAMES = {
100 0: 'offlinePassword',
106 // Focus and tab order are organized as follows:
108 // (1) all user pods have tab index 1 so they are traversed first;
109 // (2) when a user pod is activated, its tab index is set to -1 and its
110 // main input field gets focus and tab index 1;
111 // (3) buttons on the header bar have tab index 2 so they follow user pods;
112 // (4) Action box buttons have tab index 3 and follow header bar buttons;
113 // (5) lastly, focus jumps to the Status Area and back to user pods.
115 // 'Focus' event is handled by a capture handler for the whole document
116 // and in some cases 'mousedown' event handlers are used instead of 'click'
117 // handlers where it's necessary to prevent 'focus' event from being fired.
120 * Helper function to remove a class from given element.
121 * @param {!HTMLElement} el Element whose class list to change.
122 * @param {string} cl Class to remove.
124 function removeClass(el, cl) {
125 el.classList.remove(cl);
129 * Creates a user pod.
131 * @extends {HTMLDivElement}
133 var UserPod = cr.ui.define(function() {
134 var node = $('user-pod-template').cloneNode(true);
135 node.removeAttribute('id');
140 * Stops event propagation from the any user pod child element.
141 * @param {Event} e Event to handle.
143 function stopEventPropagation(e) {
144 // Prevent default so that we don't trigger a 'focus' event.
150 * Unique salt added to user image URLs to prevent caching. Dictionary with
151 * user names as keys.
154 UserPod.userImageSalt_ = {};
156 UserPod.prototype = {
157 __proto__: HTMLDivElement.prototype,
160 decorate: function() {
161 this.tabIndex = UserPodTabOrder.POD_INPUT;
162 this.customButtonElement.tabIndex = UserPodTabOrder.POD_INPUT;
163 this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
165 this.addEventListener('keydown', this.handlePodKeyDown_.bind(this));
166 this.addEventListener('click', this.handleClickOnPod_.bind(this));
168 this.signinButtonElement.addEventListener('click',
169 this.activate.bind(this));
171 this.actionBoxAreaElement.addEventListener('mousedown',
172 stopEventPropagation);
173 this.actionBoxAreaElement.addEventListener('click',
174 this.handleActionAreaButtonClick_.bind(this));
175 this.actionBoxAreaElement.addEventListener('keydown',
176 this.handleActionAreaButtonKeyDown_.bind(this));
178 this.actionBoxMenuRemoveElement.addEventListener('click',
179 this.handleRemoveCommandClick_.bind(this));
180 this.actionBoxMenuRemoveElement.addEventListener('keydown',
181 this.handleRemoveCommandKeyDown_.bind(this));
182 this.actionBoxMenuRemoveElement.addEventListener('blur',
183 this.handleRemoveCommandBlur_.bind(this));
185 if (this.actionBoxRemoveUserWarningButtonElement) {
186 this.actionBoxRemoveUserWarningButtonElement.addEventListener(
188 this.handleRemoveUserConfirmationClick_.bind(this));
191 this.customButtonElement.addEventListener('click',
192 this.handleCustomButtonClick_.bind(this));
196 * Initializes the pod after its properties set and added to a pod row.
198 initialize: function() {
199 this.passwordElement.addEventListener('keydown',
200 this.parentNode.handleKeyDown.bind(this.parentNode));
201 this.passwordElement.addEventListener('keypress',
202 this.handlePasswordKeyPress_.bind(this));
204 this.imageElement.addEventListener('load',
205 this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
207 var initialAuthType = this.user.initialAuthType ||
208 AUTH_TYPE.OFFLINE_PASSWORD;
209 this.setAuthType(initialAuthType, null);
213 * Resets tab order for pod elements to its initial state.
215 resetTabOrder: function() {
216 // Note: the |mainInput| can be the pod itself.
217 this.mainInput.tabIndex = -1;
218 this.tabIndex = UserPodTabOrder.POD_INPUT;
222 * Handles keypress event (i.e. any textual input) on password input.
223 * @param {Event} e Keypress Event object.
226 handlePasswordKeyPress_: function(e) {
227 // When tabbing from the system tray a tab key press is received. Suppress
228 // this so as not to type a tab character into the password field.
229 if (e.keyCode == 9) {
236 * Top edge margin number of pixels.
240 this.style.top = cr.ui.toCssPx(top);
244 * Top edge margin number of pixels.
247 return parseInt(this.style.top);
251 * Left edge margin number of pixels.
255 this.style.left = cr.ui.toCssPx(left);
259 * Left edge margin number of pixels.
262 return parseInt(this.style.left);
266 * Height number of pixels.
269 return this.offsetHeight;
273 * Gets signed in indicator element.
274 * @type {!HTMLDivElement}
276 get signedInIndicatorElement() {
277 return this.querySelector('.signed-in-indicator');
281 * Gets image element.
282 * @type {!HTMLImageElement}
285 return this.querySelector('.user-image');
290 * @type {!HTMLDivElement}
293 return this.querySelector('.name');
297 * Gets password field.
298 * @type {!HTMLInputElement}
300 get passwordElement() {
301 return this.querySelector('.password');
305 * Gets the password label, which is used to show a message where the
306 * password field is normally.
307 * @type {!HTMLInputElement}
309 get passwordLabelElement() {
310 return this.querySelector('.password-label');
314 * Gets Caps Lock hint image.
315 * @type {!HTMLImageElement}
317 get capslockHintElement() {
318 return this.querySelector('.capslock-hint');
322 * Gets user sign in button.
323 * @type {!HTMLButtonElement}
325 get signinButtonElement() {
326 return this.querySelector('.signin-button');
330 * Gets launch app button.
331 * @type {!HTMLButtonElement}
333 get launchAppButtonElement() {
334 return this.querySelector('.launch-app-button');
338 * Gets action box area.
339 * @type {!HTMLInputElement}
341 get actionBoxAreaElement() {
342 return this.querySelector('.action-box-area');
346 * Gets user type icon area.
347 * @type {!HTMLDivElement}
349 get userTypeIconAreaElement() {
350 return this.querySelector('.user-type-icon-area');
354 * Gets user type icon.
355 * @type {!HTMLDivElement}
357 get userTypeIconElement() {
358 return this.querySelector('.user-type-icon-image');
362 * Gets action box menu.
363 * @type {!HTMLInputElement}
365 get actionBoxMenuElement() {
366 return this.querySelector('.action-box-menu');
370 * Gets action box menu title.
371 * @type {!HTMLInputElement}
373 get actionBoxMenuTitleElement() {
374 return this.querySelector('.action-box-menu-title');
378 * Gets action box menu title, user name item.
379 * @type {!HTMLInputElement}
381 get actionBoxMenuTitleNameElement() {
382 return this.querySelector('.action-box-menu-title-name');
386 * Gets action box menu title, user email item.
387 * @type {!HTMLInputElement}
389 get actionBoxMenuTitleEmailElement() {
390 return this.querySelector('.action-box-menu-title-email');
394 * Gets action box menu, remove user command item.
395 * @type {!HTMLInputElement}
397 get actionBoxMenuCommandElement() {
398 return this.querySelector('.action-box-menu-remove-command');
402 * Gets action box menu, remove user command item div.
403 * @type {!HTMLInputElement}
405 get actionBoxMenuRemoveElement() {
406 return this.querySelector('.action-box-menu-remove');
410 * Gets action box menu, remove user command item div.
411 * @type {!HTMLInputElement}
413 get actionBoxRemoveUserWarningElement() {
414 return this.querySelector('.action-box-remove-user-warning');
418 * Gets action box menu, remove user command item div.
419 * @type {!HTMLInputElement}
421 get actionBoxRemoveUserWarningButtonElement() {
422 return this.querySelector(
423 '.remove-warning-button');
427 * Gets the locked user indicator box.
428 * @type {!HTMLInputElement}
430 get lockedIndicatorElement() {
431 return this.querySelector('.locked-indicator');
435 * Gets the custom button. This button is normally hidden, but can be shown
436 * using the chrome.screenlockPrivate API.
437 * @type {!HTMLInputElement}
439 get customButtonElement() {
440 return this.querySelector('.custom-button');
444 * Updates the user pod element.
447 this.imageElement.src = 'chrome://userimage/' + this.user.username +
448 '?id=' + UserPod.userImageSalt_[this.user.username];
450 this.nameElement.textContent = this.user_.displayName;
451 this.signedInIndicatorElement.hidden = !this.user_.signedIn;
453 this.signinButtonElement.hidden = !this.isAuthTypeOnlineSignIn;
454 this.customButtonElement.tabIndex = UserPodTabOrder.POD_INPUT;
455 if (this.isAuthTypeUserClick) {
456 this.passwordLabelElement.textContent = this.authValue;
457 this.customButtonElement.tabIndex = -1;
460 this.updateActionBoxArea();
462 this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
463 'passwordFieldAccessibleName', this.user_.emailAddress));
465 this.customizeUserPodPerUserType();
468 updateActionBoxArea: function() {
469 if (this.user_.publicAccount || this.user_.isApp) {
470 this.actionBoxAreaElement.hidden = true;
474 this.actionBoxAreaElement.hidden = false;
475 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
477 this.actionBoxAreaElement.setAttribute(
478 'aria-label', loadTimeData.getStringF(
479 'podMenuButtonAccessibleName', this.user_.emailAddress));
480 this.actionBoxMenuRemoveElement.setAttribute(
481 'aria-label', loadTimeData.getString(
482 'podMenuRemoveItemAccessibleName'));
483 this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
484 loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
485 this.user_.displayName;
486 this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
487 this.actionBoxMenuTitleEmailElement.hidden =
488 this.user_.locallyManagedUser;
490 this.actionBoxMenuCommandElement.textContent =
491 loadTimeData.getString('removeUser');
494 customizeUserPodPerUserType: function() {
495 var isMultiProfilesUI =
496 (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING);
498 if (this.user_.locallyManagedUser) {
499 this.setUserPodIconType('supervised');
500 } else if (isMultiProfilesUI && !this.user_.isMultiProfilesAllowed) {
501 // Mark user pod as not focusable which in addition to the grayed out
502 // filter makes it look in disabled state.
503 this.classList.add('not-focusable');
504 this.setUserPodIconType('policy');
506 this.querySelector('.mp-policy-title').hidden = false;
507 if (this.user.multiProfilesPolicy == 'primary-only')
508 this.querySelector('.mp-policy-primary-only-msg').hidden = false;
510 this.querySelector('.mp-policy-not-allowed-msg').hidden = false;
511 } else if (this.user_.isApp) {
512 this.setUserPodIconType('app');
516 setUserPodIconType: function(userTypeClass) {
517 this.userTypeIconAreaElement.classList.add(userTypeClass);
518 this.userTypeIconAreaElement.hidden = false;
522 * The user that this pod represents.
530 this.user_ = userDict;
535 * Gets main input element.
536 * @type {(HTMLButtonElement|HTMLInputElement)}
539 if (this.isAuthTypePassword) {
540 return this.passwordElement;
541 } else if (this.isAuthTypeOnlineSignIn) {
542 return this.signinButtonElement;
543 } else if (this.isAuthTypeUserClick) {
549 * Whether action box button is in active state.
552 get isActionBoxMenuActive() {
553 return this.actionBoxAreaElement.classList.contains('active');
555 set isActionBoxMenuActive(active) {
556 if (active == this.isActionBoxMenuActive)
560 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
561 if (this.actionBoxRemoveUserWarningElement)
562 this.actionBoxRemoveUserWarningElement.hidden = true;
564 // Clear focus first if another pod is focused.
565 if (!this.parentNode.isFocused(this)) {
566 this.parentNode.focusPod(undefined, true);
567 this.actionBoxAreaElement.focus();
569 this.actionBoxAreaElement.classList.add('active');
571 this.actionBoxAreaElement.classList.remove('active');
576 * Whether action box button is in hovered state.
579 get isActionBoxMenuHovered() {
580 return this.actionBoxAreaElement.classList.contains('hovered');
582 set isActionBoxMenuHovered(hovered) {
583 if (hovered == this.isActionBoxMenuHovered)
587 this.actionBoxAreaElement.classList.add('hovered');
588 this.classList.add('hovered');
590 this.actionBoxAreaElement.classList.remove('hovered');
591 this.classList.remove('hovered');
596 * Set the authentication type for the pod.
597 * @param {number} An auth type value defined in the AUTH_TYPE enum.
598 * @param {string} authValue The initial value used for the auth type.
600 setAuthType: function(authType, authValue) {
601 this.authType_ = authType;
602 this.authValue_ = authValue;
603 this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]);
605 this.reset(this.parentNode.isFocused(this));
609 * The auth type of the user pod. This value is one of the enum
610 * values in AUTH_TYPE.
614 return this.authType_;
618 * The initial value used for the pod's authentication type.
619 * eg. a prepopulated password input when using password authentication.
622 return this.authValue_;
626 * True if the the user pod uses a password to authenticate.
629 get isAuthTypePassword() {
630 return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD;
634 * True if the the user pod uses a user click to authenticate.
637 get isAuthTypeUserClick() {
638 return this.authType_ == AUTH_TYPE.USER_CLICK;
642 * True if the the user pod uses a online sign in to authenticate.
645 get isAuthTypeOnlineSignIn() {
646 return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN;
650 * Updates the image element of the user.
652 updateUserImage: function() {
653 UserPod.userImageSalt_[this.user.username] = new Date().getTime();
658 * Focuses on input element.
660 focusInput: function() {
661 // Move tabIndex from the whole pod to the main input.
662 // Note: the |mainInput| can be the pod itself.
664 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
665 this.mainInput.focus();
670 * @param {Event} e Event object.
671 * @return {boolean} True if activated successfully.
673 activate: function(e) {
674 if (this.isAuthTypeOnlineSignIn) {
676 } else if (this.isAuthTypeUserClick) {
677 Oobe.disableSigninUI();
678 chrome.send('authenticateUser', [this.user.username, '']);
679 } else if (this.isAuthTypePassword) {
680 if (!this.passwordElement.value)
682 Oobe.disableSigninUI();
683 chrome.send('authenticateUser',
684 [this.user.username, this.passwordElement.value]);
686 console.error('Activating user pod with invalid authentication type: ' +
693 showSupervisedUserSigninWarning: function() {
694 // Locally managed user token has been invalidated.
695 // Make sure that pod is focused i.e. "Sign in" button is seen.
696 this.parentNode.focusPod(this);
698 var error = document.createElement('div');
699 var messageDiv = document.createElement('div');
700 messageDiv.className = 'error-message-bubble';
701 messageDiv.textContent =
702 loadTimeData.getString('supervisedUserExpiredTokenWarning');
703 error.appendChild(messageDiv);
705 $('bubble').showContentForElement(
706 this.signinButtonElement,
707 cr.ui.Bubble.Attachment.TOP,
709 this.signinButtonElement.offsetWidth / 2,
714 * Shows signin UI for this user.
716 showSigninUI: function() {
717 if (this.user.locallyManagedUser) {
718 this.showSupervisedUserSigninWarning();
720 this.parentNode.showSigninUI(this.user.emailAddress);
725 * Resets the input field and updates the tab order of pod controls.
726 * @param {boolean} takeFocus If true, input field takes focus.
728 reset: function(takeFocus) {
729 this.passwordElement.value = '';
731 this.focusInput(); // This will set a custom tab order.
733 this.resetTabOrder();
737 * Handles a click event on action area button.
738 * @param {Event} e Click event.
740 handleActionAreaButtonClick_: function(e) {
741 if (this.parentNode.disabled)
743 this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
748 * Handles a keydown event on action area button.
749 * @param {Event} e KeyDown event.
751 handleActionAreaButtonKeyDown_: function(e) {
754 switch (e.keyIdentifier) {
756 case 'U+0020': // Space
757 if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
758 this.isActionBoxMenuActive = true;
763 if (this.isActionBoxMenuActive) {
764 this.actionBoxMenuRemoveElement.tabIndex =
765 UserPodTabOrder.PAD_MENU_ITEM;
766 this.actionBoxMenuRemoveElement.focus();
770 case 'U+001B': // Esc
771 this.isActionBoxMenuActive = false;
774 case 'U+0009': // Tab
775 this.parentNode.focusPod();
777 this.isActionBoxMenuActive = false;
783 * Handles a click event on remove user command.
784 * @param {Event} e Click event.
786 handleRemoveCommandClick_: function(e) {
787 if (this.user.locallyManagedUser || this.user.isDesktopUser) {
788 this.showRemoveWarning_();
791 if (this.isActionBoxMenuActive)
792 chrome.send('removeUser', [this.user.username]);
796 * Shows remove warning for managed users.
798 showRemoveWarning_: function() {
799 this.actionBoxMenuRemoveElement.hidden = true;
800 this.actionBoxRemoveUserWarningElement.hidden = false;
804 * Handles a click event on remove user confirmation button.
805 * @param {Event} e Click event.
807 handleRemoveUserConfirmationClick_: function(e) {
808 if (this.isActionBoxMenuActive)
809 chrome.send('removeUser', [this.user.username]);
813 * Handles a keydown event on remove command.
814 * @param {Event} e KeyDown event.
816 handleRemoveCommandKeyDown_: function(e) {
819 switch (e.keyIdentifier) {
821 chrome.send('removeUser', [this.user.username]);
828 case 'U+001B': // Esc
829 this.actionBoxAreaElement.focus();
830 this.isActionBoxMenuActive = false;
834 this.actionBoxAreaElement.focus();
835 this.isActionBoxMenuActive = false;
841 * Handles a blur event on remove command.
842 * @param {Event} e Blur event.
844 handleRemoveCommandBlur_: function(e) {
847 this.actionBoxMenuRemoveElement.tabIndex = -1;
851 * Handles click event on a user pod.
852 * @param {Event} e Click event.
854 handleClickOnPod_: function(e) {
855 if (this.parentNode.disabled)
858 if (!this.isActionBoxMenuActive) {
859 if (this.isAuthTypeOnlineSignIn) {
861 } else if (this.isAuthTypeUserClick) {
862 this.parentNode.setActivatedPod(this);
865 // Prevent default so that we don't trigger 'focus' event.
871 * Handles keydown event for a user pod.
872 * @param {Event} e Key event.
874 handlePodKeyDown_: function(e) {
875 if (!this.isAuthTypeUserClick || this.disabled)
877 switch (e.keyIdentifier) {
879 case 'U+0020': // Space
880 if (this.parentNode.isFocused(this))
881 this.parentNode.setActivatedPod(this);
887 * Called when the custom button is clicked.
889 handleCustomButtonClick_: function() {
890 chrome.send('customButtonClicked', [this.user.username]);
895 * Creates a public account user pod.
899 var PublicAccountUserPod = cr.ui.define(function() {
900 var node = UserPod();
902 var extras = $('public-account-user-pod-extras-template').children;
903 for (var i = 0; i < extras.length; ++i) {
904 var el = extras[i].cloneNode(true);
905 node.appendChild(el);
911 PublicAccountUserPod.prototype = {
912 __proto__: UserPod.prototype,
915 * "Enter" button in expanded side pane.
916 * @type {!HTMLButtonElement}
918 get enterButtonElement() {
919 return this.querySelector('.enter-button');
923 * Boolean flag of whether the pod is showing the side pane. The flag
924 * controls whether 'expanded' class is added to the pod's class list and
925 * resets tab order because main input element changes when the 'expanded'
930 return this.classList.contains('expanded');
934 * During transition final height of pod is not available because of
935 * flexbox layout. That's why we have to calculate
936 * the final height manually.
938 get expandedHeight_() {
939 function getTopAndBottomPadding(domElement) {
940 return parseInt(window.getComputedStyle(
941 domElement).getPropertyValue('padding-top')) +
942 parseInt(window.getComputedStyle(
943 domElement).getPropertyValue('padding-bottom'));
946 this.getElementsByClassName('side-pane-contents')[0].offsetHeight +
947 this.getElementsByClassName('enter-button')[0].offsetHeight +
948 getTopAndBottomPadding(
949 this.getElementsByClassName('enter-button')[0]) +
950 getTopAndBottomPadding(
951 this.getElementsByClassName('side-pane-container')[0]) +
952 getTopAndBottomPadding(this);
956 set expanded(expanded) {
957 if (this.expanded == expanded)
960 this.resetTabOrder();
961 this.classList.toggle('expanded', expanded);
963 this.usualLeft = this.left;
964 this.usualTop = this.top;
965 if (this.left + PUBLIC_EXPANDED_WIDTH >
966 $('pod-row').offsetWidth - POD_ROW_PADDING)
967 this.left = $('pod-row').offsetWidth - POD_ROW_PADDING -
968 PUBLIC_EXPANDED_WIDTH;
969 var expandedHeight = this.expandedHeight_;
970 if (this.top + expandedHeight > $('pod-row').offsetHeight)
971 this.top = $('pod-row').offsetHeight - expandedHeight;
973 if (typeof(this.usualLeft) != 'undefined')
974 this.left = this.usualLeft;
975 if (typeof(this.usualTop) != 'undefined')
976 this.top = this.usualTop;
980 this.classList.add('animating');
981 this.addEventListener('webkitTransitionEnd', function f(e) {
982 self.removeEventListener('webkitTransitionEnd', f);
983 self.classList.remove('animating');
985 // Accessibility focus indicator does not move with the focused
986 // element. Sends a 'focus' event on the currently focused element
987 // so that accessibility focus indicator updates its location.
988 if (document.activeElement)
989 document.activeElement.dispatchEvent(new Event('focus'));
996 return this.enterButtonElement;
998 return this.nameElement;
1002 decorate: function() {
1003 UserPod.prototype.decorate.call(this);
1005 this.classList.remove('need-password');
1006 this.classList.add('public-account');
1008 this.nameElement.addEventListener('keydown', (function(e) {
1009 if (e.keyIdentifier == 'Enter') {
1010 this.parentNode.setActivatedPod(this, e);
1011 // Stop this keydown event from bubbling up to PodRow handler.
1012 e.stopPropagation();
1013 // Prevent default so that we don't trigger a 'click' event on the
1014 // newly focused "Enter" button.
1019 var learnMore = this.querySelector('.learn-more');
1020 learnMore.addEventListener('mousedown', stopEventPropagation);
1021 learnMore.addEventListener('click', this.handleLearnMoreEvent);
1022 learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1024 learnMore = this.querySelector('.side-pane-learn-more');
1025 learnMore.addEventListener('click', this.handleLearnMoreEvent);
1026 learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1028 this.enterButtonElement.addEventListener('click', (function(e) {
1029 this.enterButtonElement.disabled = true;
1030 chrome.send('launchPublicAccount', [this.user.username]);
1035 update: function() {
1036 UserPod.prototype.update.call(this);
1037 this.querySelector('.side-pane-name').textContent =
1038 this.user_.displayName;
1039 this.querySelector('.info').textContent =
1040 loadTimeData.getStringF('publicAccountInfoFormat',
1041 this.user_.enterpriseDomain);
1045 focusInput: function() {
1046 // Move tabIndex from the whole pod to the main input.
1048 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1049 this.mainInput.focus();
1053 reset: function(takeFocus) {
1055 this.expanded = false;
1056 this.enterButtonElement.disabled = false;
1057 UserPod.prototype.reset.call(this, takeFocus);
1061 activate: function(e) {
1062 this.expanded = true;
1068 handleClickOnPod_: function(e) {
1069 if (this.parentNode.disabled)
1072 this.parentNode.focusPod(this);
1073 this.parentNode.setActivatedPod(this, e);
1074 // Prevent default so that we don't trigger 'focus' event.
1079 * Handle mouse and keyboard events for the learn more button. Triggering
1080 * the button causes information about public sessions to be shown.
1081 * @param {Event} event Mouse or keyboard event.
1083 handleLearnMoreEvent: function(event) {
1084 switch (event.type) {
1085 // Show informaton on left click. Let any other clicks propagate.
1087 if (event.button != 0)
1090 // Show informaton when <Return> or <Space> is pressed. Let any other
1091 // key presses propagate.
1093 switch (event.keyCode) {
1102 chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
1103 stopEventPropagation(event);
1108 * Creates a user pod to be used only in desktop chrome.
1110 * @extends {UserPod}
1112 var DesktopUserPod = cr.ui.define(function() {
1113 // Don't just instantiate a UserPod(), as this will call decorate() on the
1114 // parent object, and add duplicate event listeners.
1115 var node = $('user-pod-template').cloneNode(true);
1116 node.removeAttribute('id');
1120 DesktopUserPod.prototype = {
1121 __proto__: UserPod.prototype,
1125 if (!this.passwordElement.hidden)
1126 return this.passwordElement;
1128 return this.nameElement;
1132 decorate: function() {
1133 UserPod.prototype.decorate.call(this);
1137 update: function() {
1138 this.imageElement.src = this.user.userImage;
1139 this.nameElement.textContent = this.user.displayName;
1141 var isLockedUser = this.user.needsSignin;
1142 this.signinButtonElement.hidden = true;
1143 this.lockedIndicatorElement.hidden = !isLockedUser;
1144 this.passwordElement.hidden = !isLockedUser;
1145 this.nameElement.hidden = isLockedUser;
1147 if (this.isAuthTypeUserClick) {
1148 this.passwordLabelElement.textContent = this.authValue;
1149 this.customButtonElement.tabIndex = -1;
1152 UserPod.prototype.updateActionBoxArea.call(this);
1156 focusInput: function() {
1157 // For focused pods, display the name unless the pod is locked.
1158 var isLockedUser = this.user.needsSignin;
1159 this.signinButtonElement.hidden = true;
1160 this.lockedIndicatorElement.hidden = !isLockedUser;
1161 this.passwordElement.hidden = !isLockedUser;
1162 this.nameElement.hidden = isLockedUser;
1164 // Move tabIndex from the whole pod to the main input.
1166 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1167 this.mainInput.focus();
1171 reset: function(takeFocus) {
1172 // Always display the user's name for unfocused pods.
1174 this.nameElement.hidden = false;
1175 UserPod.prototype.reset.call(this, takeFocus);
1179 activate: function(e) {
1180 if (this.passwordElement.hidden) {
1181 Oobe.launchUser(this.user.emailAddress, this.user.displayName);
1182 } else if (!this.passwordElement.value) {
1185 chrome.send('authenticatedLaunchUser',
1186 [this.user.emailAddress,
1187 this.user.displayName,
1188 this.passwordElement.value]);
1190 this.passwordElement.value = '';
1195 handleClickOnPod_: function(e) {
1196 if (this.parentNode.disabled)
1200 this.parentNode.lastFocusedPod_ = this;
1202 // If this is an unlocked pod, then open a browser window. Otherwise
1203 // just activate the pod and show the password field.
1204 if (!this.user.needsSignin && !this.isActionBoxMenuActive)
1207 if (this.isAuthTypeUserClick)
1208 chrome.send('attemptUnlock', [this.user.emailAddress]);
1212 handleRemoveUserConfirmationClick_: function(e) {
1213 chrome.send('removeUser', [this.user.profilePath]);
1218 * Creates a user pod that represents kiosk app.
1220 * @extends {UserPod}
1222 var KioskAppPod = cr.ui.define(function() {
1223 var node = UserPod();
1227 KioskAppPod.prototype = {
1228 __proto__: UserPod.prototype,
1231 decorate: function() {
1232 UserPod.prototype.decorate.call(this);
1233 this.launchAppButtonElement.addEventListener('click',
1234 this.activate.bind(this));
1238 update: function() {
1239 this.imageElement.src = this.user.iconUrl;
1240 if (this.user.iconHeight && this.user.iconWidth) {
1241 this.imageElement.style.height = this.user.iconHeight;
1242 this.imageElement.style.width = this.user.iconWidth;
1244 this.imageElement.alt = this.user.label;
1245 this.imageElement.title = this.user.label;
1246 this.passwordElement.hidden = true;
1247 this.signinButtonElement.hidden = true;
1248 this.launchAppButtonElement.hidden = false;
1249 this.signedInIndicatorElement.hidden = true;
1250 this.nameElement.textContent = this.user.label;
1252 UserPod.prototype.updateActionBoxArea.call(this);
1253 UserPod.prototype.customizeUserPodPerUserType.call(this);
1258 return this.launchAppButtonElement;
1262 focusInput: function() {
1263 this.signinButtonElement.hidden = true;
1264 this.launchAppButtonElement.hidden = false;
1265 this.passwordElement.hidden = true;
1267 // Move tabIndex from the whole pod to the main input.
1269 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1270 this.mainInput.focus();
1274 get forceOnlineSignin() {
1279 activate: function(e) {
1280 var diagnosticMode = e && e.ctrlKey;
1281 this.launchApp_(this.user, diagnosticMode);
1286 handleClickOnPod_: function(e) {
1287 if (this.parentNode.disabled)
1291 this.parentNode.lastFocusedPod_ = this;
1296 * Launch the app. If |diagnosticMode| is true, ask user to confirm.
1297 * @param {Object} app App data.
1298 * @param {boolean} diagnosticMode Whether to run the app in diagnostic
1301 launchApp_: function(app, diagnosticMode) {
1302 if (!diagnosticMode) {
1303 chrome.send('launchKioskApp', [app.id, false]);
1307 var oobe = $('oobe');
1308 if (!oobe.confirmDiagnosticMode_) {
1309 oobe.confirmDiagnosticMode_ =
1310 new cr.ui.dialogs.ConfirmDialog(document.body);
1311 oobe.confirmDiagnosticMode_.setOkLabel(
1312 loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
1313 oobe.confirmDiagnosticMode_.setCancelLabel(
1314 loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
1317 oobe.confirmDiagnosticMode_.show(
1318 loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
1321 chrome.send('launchKioskApp', [app.id, true]);
1327 * Creates a new pod row element.
1329 * @extends {HTMLDivElement}
1331 var PodRow = cr.ui.define('podrow');
1333 PodRow.prototype = {
1334 __proto__: HTMLDivElement.prototype,
1336 // Whether this user pod row is shown for the first time.
1339 // True if inside focusPod().
1340 insideFocusPod_: false,
1343 focusedPod_: undefined,
1345 // Activated pod, i.e. the pod of current login attempt.
1346 activatedPod_: undefined,
1348 // Pod that was most recently focused, if any.
1349 lastFocusedPod_: undefined,
1351 // Pods whose initial images haven't been loaded yet.
1352 podsWithPendingImages_: [],
1354 // Whether pod creation is animated.
1355 userAddIsAnimated_: false,
1357 // Whether pod placement has been postponed.
1358 podPlacementPostponed_: false,
1360 // Standard user pod height/width.
1364 // Array of apps that are shown in addition to other user pods.
1367 // True to show app pods along with user pods.
1368 shouldShowApps_: true,
1370 // Array of users that are shown (public/supervised/regular).
1374 decorate: function() {
1375 // Event listeners that are installed for the time period during which
1376 // the element is visible.
1378 focus: [this.handleFocus_.bind(this), true /* useCapture */],
1379 click: [this.handleClick_.bind(this), true],
1380 mousemove: [this.handleMouseMove_.bind(this), false],
1381 keydown: [this.handleKeyDown.bind(this), false]
1384 var isDesktopUserManager = Oobe.getInstance().displayType ==
1385 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1386 this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT :
1388 // Same for Chrome OS and desktop.
1389 this.userPodWidth_ = POD_WIDTH;
1393 * Returns all the pods in this pod row.
1397 return Array.prototype.slice.call(this.children);
1401 * Return true if user pod row has only single user pod in it.
1405 return this.children.length == 1;
1409 * Returns pod with the given app id.
1410 * @param {!string} app_id Application id to be matched.
1411 * @return {Object} Pod with the given app id. null if pod hasn't been
1414 getPodWithAppId_: function(app_id) {
1415 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1416 if (pod.user.isApp && pod.user.id == app_id)
1423 * Returns pod with the given username (null if there is no such pod).
1424 * @param {string} username Username to be matched.
1425 * @return {Object} Pod with the given username. null if pod hasn't been
1428 getPodWithUsername_: function(username) {
1429 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1430 if (pod.user.username == username)
1437 * True if the the pod row is disabled (handles no user interaction).
1442 return this.disabled_;
1444 set disabled(value) {
1445 this.disabled_ = value;
1446 var controls = this.querySelectorAll('button,input');
1447 for (var i = 0, control; control = controls[i]; ++i) {
1448 control.disabled = value;
1453 * Creates a user pod from given email.
1454 * @param {!Object} user User info dictionary.
1456 createUserPod: function(user) {
1458 if (user.isDesktopUser)
1459 userPod = new DesktopUserPod({user: user});
1460 else if (user.publicAccount)
1461 userPod = new PublicAccountUserPod({user: user});
1462 else if (user.isApp)
1463 userPod = new KioskAppPod({user: user});
1465 userPod = new UserPod({user: user});
1467 userPod.hidden = false;
1472 * Add an existing user pod to this pod row.
1473 * @param {!Object} user User info dictionary.
1474 * @param {boolean} animated Whether to use init animation.
1476 addUserPod: function(user, animated) {
1477 var userPod = this.createUserPod(user);
1479 userPod.classList.add('init');
1480 userPod.nameElement.classList.add('init');
1483 this.appendChild(userPod);
1484 userPod.initialize();
1488 * Runs app with a given id from the list of loaded apps.
1489 * @param {!string} app_id of an app to run.
1490 * @param {boolean=} opt_diagnostic_mode Whether to run the app in
1491 * diagnostic mode. Default is false.
1493 findAndRunAppForTesting: function(app_id, opt_diagnostic_mode) {
1494 var app = this.getPodWithAppId_(app_id);
1496 var activationEvent = cr.doc.createEvent('MouseEvents');
1497 var ctrlKey = opt_diagnostic_mode;
1498 activationEvent.initMouseEvent('click', true, true, null,
1499 0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
1500 app.dispatchEvent(activationEvent);
1505 * Removes user pod from pod row.
1506 * @param {string} email User's email.
1508 removeUserPod: function(username) {
1509 var podToRemove = this.getPodWithUsername_(username);
1510 if (podToRemove == null) {
1511 console.warn('Attempt to remove not existing pod for ' + username +
1515 this.removeChild(podToRemove);
1516 if (this.pods.length > 0)
1521 * Returns index of given pod or -1 if not found.
1522 * @param {UserPod} pod Pod to look up.
1525 indexOf_: function(pod) {
1526 for (var i = 0; i < this.pods.length; ++i) {
1527 if (pod == this.pods[i])
1534 * Start first time show animation.
1536 startInitAnimation: function() {
1537 // Schedule init animation.
1538 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1539 window.setTimeout(removeClass, 500 + i * 70, pod, 'init');
1540 window.setTimeout(removeClass, 700 + i * 70, pod.nameElement, 'init');
1545 * Start login success animation.
1547 startAuthenticatedAnimation: function() {
1548 var activated = this.indexOf_(this.activatedPod_);
1549 if (activated == -1)
1552 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1554 pod.classList.add('left');
1555 else if (i > activated)
1556 pod.classList.add('right');
1558 pod.classList.add('zoom');
1563 * Populates pod row with given existing users and start init animation.
1564 * @param {array} users Array of existing user emails.
1565 * @param {boolean} animated Whether to use init animation.
1567 loadPods: function(users, animated) {
1568 this.users_ = users;
1569 this.userAddIsAnimated_ = animated;
1575 * Rebuilds pod row using users_ and apps_ that were previously set or
1578 rebuildPods: function() {
1579 var emptyPodRow = this.pods.length == 0;
1581 // Clear existing pods.
1582 this.innerHTML = '';
1583 this.focusedPod_ = undefined;
1584 this.activatedPod_ = undefined;
1585 this.lastFocusedPod_ = undefined;
1587 // Switch off animation
1588 Oobe.getInstance().toggleClass('flying-pods', false);
1590 // Populate the pod row.
1591 for (var i = 0; i < this.users_.length; ++i)
1592 this.addUserPod(this.users_[i], this.userAddIsAnimated_);
1594 for (var i = 0, pod; pod = this.pods[i]; ++i)
1595 this.podsWithPendingImages_.push(pod);
1597 // TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
1598 if (this.shouldShowApps_) {
1599 for (var i = 0; i < this.apps_.length; ++i)
1600 this.addUserPod(this.apps_[i], this.userAddIsAnimated_);
1603 // Make sure we eventually show the pod row, even if some image is stuck.
1604 setTimeout(function() {
1605 $('pod-row').classList.remove('images-loading');
1606 }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
1608 var isCrosAccountPicker = $('login-header-bar').signinUIState ==
1609 SIGNIN_UI_STATE.ACCOUNT_PICKER;
1610 var isDesktopUserManager = Oobe.getInstance().displayType ==
1611 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1613 // Chrome OS: immediately recalculate pods layout only when current UI
1614 // is account picker. Otherwise postpone it.
1615 // Desktop: recalculate pods layout right away.
1616 if (isDesktopUserManager || isCrosAccountPicker) {
1619 // Without timeout changes in pods positions will be animated even
1620 // though it happened when 'flying-pods' class was disabled.
1621 setTimeout(function() {
1622 Oobe.getInstance().toggleClass('flying-pods', true);
1625 this.focusPod(this.preselectedPod);
1627 this.podPlacementPostponed_ = true;
1629 // Update [Cancel] button state.
1630 if ($('login-header-bar').signinUIState ==
1631 SIGNIN_UI_STATE.GAIA_SIGNIN &&
1633 this.pods.length > 0) {
1634 login.GaiaSigninScreen.updateCancelButtonState();
1640 * Adds given apps to the pod row.
1641 * @param {array} apps Array of apps.
1643 setApps: function(apps) {
1646 chrome.send('kioskAppsLoaded');
1648 // Check whether there's a pending kiosk app error.
1649 window.setTimeout(function() {
1650 chrome.send('checkKioskAppLaunchError');
1655 * Sets whether should show app pods.
1656 * @param {boolean} shouldShowApps Whether app pods should be shown.
1658 setShouldShowApps: function(shouldShowApps) {
1659 if (this.shouldShowApps_ == shouldShowApps)
1662 this.shouldShowApps_ = shouldShowApps;
1667 * Shows a button on a user pod with an icon. Clicking on this button
1668 * triggers an event used by the chrome.screenlockPrivate API.
1669 * @param {string} username Username of pod to add button
1670 * @param {string} iconURL URL of the button icon
1672 showUserPodButton: function(username, iconURL) {
1673 var pod = this.getPodWithUsername_(username);
1675 console.error('Unable to show user pod button for ' + username +
1676 ': user pod not found.');
1680 pod.customButtonElement.hidden = false;
1682 pod.customButtonElement.querySelector('.custom-button-icon');
1687 * Hides button from user pod added by showUserPodButton().
1688 * @param {string} username Username of pod to remove button
1690 hideUserPodButton: function(username) {
1691 var pod = this.getPodWithUsername_(username);
1693 console.error('Unable to hide user pod button for ' + username +
1694 ': user pod not found.');
1698 pod.customButtonElement.hidden = true;
1702 * Sets the authentication type used to authenticate the user.
1703 * @param {string} username Username of selected user
1704 * @param {number} authType Authentication type, must be one of the
1705 * values listed in AUTH_TYPE enum.
1706 * @param {string} value The initial value to use for authentication.
1708 setAuthType: function(username, authType, value) {
1709 var pod = this.getPodWithUsername_(username);
1711 console.error('Unable to set auth type for ' + username +
1712 ': user pod not found.');
1715 pod.setAuthType(authType, value);
1719 * Shows a tooltip bubble explaining Easy Unlock for the focused pod.
1721 showEasyUnlockBubble: function() {
1722 if (!this.focusedPod_) {
1723 console.error('No focused pod to show Easy Unlock bubble.');
1727 var bubbleContent = document.createElement('div');
1728 bubbleContent.classList.add('easy-unlock-button-content');
1729 bubbleContent.textContent = loadTimeData.getString('easyUnlockTooltip');
1731 var attachElement = this.focusedPod_.customButtonElement;
1732 /** @const */ var BUBBLE_OFFSET = 20;
1733 /** @const */ var BUBBLE_PADDING = 8;
1734 $('bubble').showContentForElement(attachElement,
1735 cr.ui.Bubble.Attachment.RIGHT,
1742 * Called when window was resized.
1744 onWindowResize: function() {
1745 var layout = this.calculateLayout_();
1746 if (layout.columns != this.columns || layout.rows != this.rows)
1751 * Returns width of podrow having |columns| number of columns.
1754 columnsToWidth_: function(columns) {
1755 var margin = MARGIN_BY_COLUMNS[columns];
1756 return 2 * POD_ROW_PADDING + columns *
1757 this.userPodWidth_ + (columns - 1) * margin;
1761 * Returns height of podrow having |rows| number of rows.
1764 rowsToHeight_: function(rows) {
1765 return 2 * POD_ROW_PADDING + rows * this.userPodHeight_;
1769 * Calculates number of columns and rows that podrow should have in order to
1770 * hold as much its pods as possible for current screen size. Also it tries
1771 * to choose layout that looks good.
1772 * @return {{columns: number, rows: number}}
1774 calculateLayout_: function() {
1775 var preferredColumns = this.pods.length < COLUMNS.length ?
1776 COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
1777 var maxWidth = Oobe.getInstance().clientAreaSize.width;
1778 var columns = preferredColumns;
1779 while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
1781 var rows = Math.floor((this.pods.length - 1) / columns) + 1;
1782 if (getComputedStyle(
1783 $('signin-banner'), null).getPropertyValue('display') != 'none') {
1784 rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER);
1786 var maxHeigth = Oobe.getInstance().clientAreaSize.height;
1787 while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
1789 // One more iteration if it's not enough cells to place all pods.
1790 while (maxWidth >= this.columnsToWidth_(columns + 1) &&
1791 columns * rows < this.pods.length &&
1792 columns < MAX_NUMBER_OF_COLUMNS) {
1795 return {columns: columns, rows: rows};
1799 * Places pods onto their positions onto pod grid.
1802 placePods_: function() {
1803 var layout = this.calculateLayout_();
1804 var columns = this.columns = layout.columns;
1805 var rows = this.rows = layout.rows;
1806 var maxPodsNumber = columns * rows;
1807 var margin = MARGIN_BY_COLUMNS[columns];
1808 this.parentNode.setPreferredSize(
1809 this.columnsToWidth_(columns), this.rowsToHeight_(rows));
1810 var height = this.userPodHeight_;
1811 var width = this.userPodWidth_;
1812 this.pods.forEach(function(pod, index) {
1813 if (pod.offsetHeight != height) {
1814 console.error('Pod offsetHeight (' + pod.offsetHeight +
1815 ') and POD_HEIGHT (' + height + ') are not equal.');
1817 if (pod.offsetWidth != width) {
1818 console.error('Pod offsetWidth (' + pod.offsetWidth +
1819 ') and POD_WIDTH (' + width + ') are not equal.');
1821 if (index >= maxPodsNumber) {
1826 var column = index % columns;
1827 var row = Math.floor(index / columns);
1828 pod.left = POD_ROW_PADDING + column * (width + margin);
1829 pod.top = POD_ROW_PADDING + row * height;
1831 Oobe.getInstance().updateScreenSize(this.parentNode);
1835 * Number of columns.
1838 set columns(columns) {
1839 // Cannot use 'columns' here.
1840 this.setAttribute('ncolumns', columns);
1843 return this.getAttribute('ncolumns');
1851 // Cannot use 'rows' here.
1852 this.setAttribute('nrows', rows);
1855 return this.getAttribute('nrows');
1859 * Whether the pod is currently focused.
1860 * @param {UserPod} pod Pod to check for focus.
1861 * @return {boolean} Pod focus status.
1863 isFocused: function(pod) {
1864 return this.focusedPod_ == pod;
1868 * Focuses a given user pod or clear focus when given null.
1869 * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
1870 * @param {boolean=} opt_force If true, forces focus update even when
1871 * podToFocus is already focused.
1873 focusPod: function(podToFocus, opt_force) {
1874 if (this.isFocused(podToFocus) && !opt_force) {
1875 this.keyboardActivated_ = false;
1879 // Make sure that we don't focus pods that are not allowed to be focused.
1880 // TODO(nkostylev): Fix various keyboard focus related issues caused
1881 // by this approach. http://crbug.com/339042
1882 if (podToFocus && podToFocus.classList.contains('not-focusable')) {
1883 this.keyboardActivated_ = false;
1887 // Make sure there's only one focusPod operation happening at a time.
1888 if (this.insideFocusPod_) {
1889 this.keyboardActivated_ = false;
1892 this.insideFocusPod_ = true;
1894 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1895 if (!this.isSinglePod) {
1896 pod.isActionBoxMenuActive = false;
1898 if (pod != podToFocus) {
1899 pod.isActionBoxMenuHovered = false;
1900 pod.classList.remove('focused');
1901 pod.classList.remove('faded');
1906 // Clear any error messages for previous pod.
1907 if (!this.isFocused(podToFocus))
1910 var hadFocus = !!this.focusedPod_;
1911 this.focusedPod_ = podToFocus;
1913 podToFocus.classList.remove('faded');
1914 podToFocus.classList.add('focused');
1915 podToFocus.reset(true); // Reset and give focus.
1916 // focusPod() automatically loads wallpaper
1917 if (!podToFocus.user.isApp)
1918 chrome.send('focusPod', [podToFocus.user.username]);
1919 this.firstShown_ = false;
1920 this.lastFocusedPod_ = podToFocus;
1922 this.insideFocusPod_ = false;
1923 this.keyboardActivated_ = false;
1927 * Focuses a given user pod by index or clear focus when given null.
1928 * @param {int=} podToFocus index of User pod to focus.
1929 * @param {boolean=} opt_force If true, forces focus update even when
1930 * podToFocus is already focused.
1932 focusPodByIndex: function(podToFocus, opt_force) {
1933 if (podToFocus < this.pods.length)
1934 this.focusPod(this.pods[podToFocus], opt_force);
1938 * Resets wallpaper to the last active user's wallpaper, if any.
1940 loadLastWallpaper: function() {
1941 if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp)
1942 chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
1946 * Returns the currently activated pod.
1949 get activatedPod() {
1950 return this.activatedPod_;
1954 * Sets currently activated pod.
1955 * @param {UserPod} pod Pod to check for focus.
1956 * @param {Event} e Event object.
1958 setActivatedPod: function(pod, e) {
1959 if (pod && pod.activate(e))
1960 this.activatedPod_ = pod;
1964 * The pod of the signed-in user, if any; null otherwise.
1968 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1969 if (pod.user.signedIn)
1976 * The pod that is preselected on user pod row show.
1979 get preselectedPod() {
1980 var lockedPod = this.lockedPod;
1981 var preselectedPod = PRESELECT_FIRST_POD ?
1982 lockedPod || this.pods[0] : lockedPod;
1983 return preselectedPod;
1988 * @param {boolean} takeFocus True to take focus.
1990 reset: function(takeFocus) {
1991 this.disabled = false;
1992 if (this.activatedPod_)
1993 this.activatedPod_.reset(takeFocus);
1997 * Restores input focus to current selected pod, if there is any.
1999 refocusCurrentPod: function() {
2000 if (this.focusedPod_) {
2001 this.focusedPod_.focusInput();
2006 * Clears focused pod password field.
2008 clearFocusedPod: function() {
2009 if (!this.disabled && this.focusedPod_)
2010 this.focusedPod_.reset(true);
2015 * @param {string} email Email for signin UI.
2017 showSigninUI: function(email) {
2018 // Clear any error messages that might still be around.
2020 this.disabled = true;
2021 this.lastFocusedPod_ = this.getPodWithUsername_(email);
2022 Oobe.showSigninUI(email);
2026 * Updates current image of a user.
2027 * @param {string} username User for which to update the image.
2029 updateUserImage: function(username) {
2030 var pod = this.getPodWithUsername_(username);
2032 pod.updateUserImage();
2036 * Handler of click event.
2037 * @param {Event} e Click Event object.
2040 handleClick_: function(e) {
2044 // Clear all menus if the click is outside pod menu and its
2046 if (!findAncestorByClass(e.target, 'action-box-menu') &&
2047 !findAncestorByClass(e.target, 'action-box-area')) {
2048 for (var i = 0, pod; pod = this.pods[i]; ++i)
2049 pod.isActionBoxMenuActive = false;
2052 // Clears focus if not clicked on a pod and if there's more than one pod.
2053 var pod = findAncestorByClass(e.target, 'pod');
2054 if ((!pod || pod.parentNode != this) && !this.isSinglePod) {
2059 pod.isActionBoxMenuHovered = true;
2061 // Return focus back to single pod.
2062 if (this.isSinglePod) {
2063 this.focusPod(this.focusedPod_, true /* force */);
2065 this.focusedPod_.isActionBoxMenuHovered = false;
2070 * Handler of mouse move event.
2071 * @param {Event} e Click Event object.
2074 handleMouseMove_: function(e) {
2077 if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
2080 // Defocus (thus hide) action box, if it is focused on a user pod
2081 // and the pointer is not hovering over it.
2082 var pod = findAncestorByClass(e.target, 'pod');
2083 if (document.activeElement &&
2084 document.activeElement.parentNode != pod &&
2085 document.activeElement.classList.contains('action-box-area')) {
2086 document.activeElement.parentNode.focus();
2090 pod.isActionBoxMenuHovered = true;
2092 // Hide action boxes on other user pods.
2093 for (var i = 0, p; p = this.pods[i]; ++i)
2094 if (p != pod && !p.isActionBoxMenuActive)
2095 p.isActionBoxMenuHovered = false;
2099 * Handles focus event.
2100 * @param {Event} e Focus Event object.
2103 handleFocus_: function(e) {
2106 if (e.target.parentNode == this) {
2108 if (e.target.classList.contains('focused'))
2109 e.target.focusInput();
2111 this.focusPod(e.target);
2115 var pod = findAncestorByClass(e.target, 'pod');
2116 if (pod && pod.parentNode == this) {
2117 // Focus on a control of a pod but not on the action area button.
2118 if (!pod.classList.contains('focused') &&
2119 !e.target.classList.contains('action-box-button')) {
2126 // Clears pod focus when we reach here. It means new focus is neither
2127 // on a pod nor on a button/input for a pod.
2128 // Do not "defocus" user pod when it is a single pod.
2129 // That means that 'focused' class will not be removed and
2130 // input field/button will always be visible.
2131 if (!this.isSinglePod)
2136 * Handler of keydown event.
2137 * @param {Event} e KeyDown Event object.
2139 handleKeyDown: function(e) {
2142 var editing = e.target.tagName == 'INPUT' && e.target.value;
2143 switch (e.keyIdentifier) {
2146 this.keyboardActivated_ = true;
2147 if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
2148 this.focusPod(this.focusedPod_.previousElementSibling);
2150 this.focusPod(this.lastElementChild);
2152 e.stopPropagation();
2157 this.keyboardActivated_ = true;
2158 if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
2159 this.focusPod(this.focusedPod_.nextElementSibling);
2161 this.focusPod(this.firstElementChild);
2163 e.stopPropagation();
2167 if (this.focusedPod_) {
2168 var targetTag = e.target.tagName;
2169 if (e.target == this.focusedPod_.passwordElement ||
2170 (targetTag != 'INPUT' &&
2171 targetTag != 'BUTTON' &&
2172 targetTag != 'A')) {
2173 this.setActivatedPod(this.focusedPod_, e);
2174 e.stopPropagation();
2178 case 'U+001B': // Esc
2179 if (!this.isSinglePod)
2186 * Called right after the pod row is shown.
2188 handleAfterShow: function() {
2189 // Without timeout changes in pods positions will be animated even though
2190 // it happened when 'flying-pods' class was disabled.
2191 setTimeout(function() {
2192 Oobe.getInstance().toggleClass('flying-pods', true);
2194 // Force input focus for user pod on show and once transition ends.
2195 if (this.focusedPod_) {
2196 var focusedPod = this.focusedPod_;
2197 var screen = this.parentNode;
2199 focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
2200 focusedPod.removeEventListener('webkitTransitionEnd', f);
2201 focusedPod.reset(true);
2202 // Notify screen that it is ready.
2205 // Guard timer for 1 second -- it would conver all possible animations.
2206 ensureTransitionEndEvent(focusedPod, 1000);
2211 * Called right before the pod row is shown.
2213 handleBeforeShow: function() {
2214 Oobe.getInstance().toggleClass('flying-pods', false);
2215 for (var event in this.listeners_) {
2216 this.ownerDocument.addEventListener(
2217 event, this.listeners_[event][0], this.listeners_[event][1]);
2219 $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
2221 if (this.podPlacementPostponed_) {
2222 this.podPlacementPostponed_ = false;
2224 this.focusPod(this.preselectedPod);
2229 * Called when the element is hidden.
2231 handleHide: function() {
2232 for (var event in this.listeners_) {
2233 this.ownerDocument.removeEventListener(
2234 event, this.listeners_[event][0], this.listeners_[event][1]);
2236 $('login-header-bar').buttonsTabIndex = 0;
2240 * Called when a pod's user image finishes loading.
2242 handlePodImageLoad: function(pod) {
2243 var index = this.podsWithPendingImages_.indexOf(pod);
2248 this.podsWithPendingImages_.splice(index, 1);
2249 if (this.podsWithPendingImages_.length == 0) {
2250 this.classList.remove('images-loading');