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 <include src="wallpaper_loader.js"></include>
8 * @fileoverview User pod row implementation.
11 cr.define('login', function() {
13 * Number of displayed columns depending on user pod count.
14 * @type {Array.<number>}
17 var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6];
20 * Mapping between number of columns in pod-row and margin between user pods
22 * @type {Array.<number>}
25 var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12];
28 * Maximal number of columns currently supported by pod-row.
32 var MAX_NUMBER_OF_COLUMNS = 6;
35 * Variables used for pod placement processing.
36 * Width and height should be synced with computed CSS sizes of pods.
40 var POD_ROW_PADDING = 10;
43 * Whether to preselect the first pod automatically on login screen.
47 var PRESELECT_FIRST_POD = true;
50 * Maximum time for which the pod row remains hidden until all user images
55 var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
58 * Public session help topic identifier.
62 var HELP_TOPIC_PUBLIC_SESSION = 3041033;
65 * Oauth token status. These must match UserManager::OAuthTokenStatus.
69 var OAuthTokenStatus = {
78 * Tab order for user pods. Update these when adding new controls.
82 var UserPodTabOrder = {
83 POD_INPUT: 1, // Password input fields (and whole pods themselves).
84 HEADER_BAR: 2, // Buttons on the header bar (Shutdown, Add User).
85 ACTION_BOX: 3, // Action box buttons.
86 PAD_MENU_ITEM: 4 // User pad menu items (Remove this user).
89 // Focus and tab order are organized as follows:
91 // (1) all user pods have tab index 1 so they are traversed first;
92 // (2) when a user pod is activated, its tab index is set to -1 and its
93 // main input field gets focus and tab index 1;
94 // (3) buttons on the header bar have tab index 2 so they follow user pods;
95 // (4) Action box buttons have tab index 3 and follow header bar buttons;
96 // (5) lastly, focus jumps to the Status Area and back to user pods.
98 // 'Focus' event is handled by a capture handler for the whole document
99 // and in some cases 'mousedown' event handlers are used instead of 'click'
100 // handlers where it's necessary to prevent 'focus' event from being fired.
103 * Helper function to remove a class from given element.
104 * @param {!HTMLElement} el Element whose class list to change.
105 * @param {string} cl Class to remove.
107 function removeClass(el, cl) {
108 el.classList.remove(cl);
112 * Creates a user pod.
114 * @extends {HTMLDivElement}
116 var UserPod = cr.ui.define(function() {
117 var node = $('user-pod-template').cloneNode(true);
118 node.removeAttribute('id');
123 * Stops event propagation from the any user pod child element.
124 * @param {Event} e Event to handle.
126 function stopEventPropagation(e) {
127 // Prevent default so that we don't trigger a 'focus' event.
133 * Unique salt added to user image URLs to prevent caching. Dictionary with
134 * user names as keys.
137 UserPod.userImageSalt_ = {};
139 UserPod.prototype = {
140 __proto__: HTMLDivElement.prototype,
143 decorate: function() {
144 this.tabIndex = UserPodTabOrder.POD_INPUT;
145 this.customButton.tabIndex = UserPodTabOrder.POD_INPUT;
146 this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
148 // Mousedown has to be used instead of click to be able to prevent 'focus'
150 this.addEventListener('mousedown',
151 this.handleMouseDown_.bind(this));
153 this.signinButtonElement.addEventListener('click',
154 this.activate.bind(this));
156 this.actionBoxAreaElement.addEventListener('mousedown',
157 stopEventPropagation);
158 this.actionBoxAreaElement.addEventListener('click',
159 this.handleActionAreaButtonClick_.bind(this));
160 this.actionBoxAreaElement.addEventListener('keydown',
161 this.handleActionAreaButtonKeyDown_.bind(this));
163 this.actionBoxMenuRemoveElement.addEventListener('click',
164 this.handleRemoveCommandClick_.bind(this));
165 this.actionBoxMenuRemoveElement.addEventListener('keydown',
166 this.handleRemoveCommandKeyDown_.bind(this));
167 this.actionBoxMenuRemoveElement.addEventListener('blur',
168 this.handleRemoveCommandBlur_.bind(this));
170 if (this.actionBoxRemoveUserWarningButtonElement) {
171 this.actionBoxRemoveUserWarningButtonElement.addEventListener(
173 this.handleRemoveUserConfirmationClick_.bind(this));
176 this.customButton.addEventListener('click',
177 this.handleCustomButtonClick_.bind(this));
181 * Initializes the pod after its properties set and added to a pod row.
183 initialize: function() {
184 this.passwordElement.addEventListener('keydown',
185 this.parentNode.handleKeyDown.bind(this.parentNode));
186 this.passwordElement.addEventListener('keypress',
187 this.handlePasswordKeyPress_.bind(this));
189 this.imageElement.addEventListener('load',
190 this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
194 * Resets tab order for pod elements to its initial state.
196 resetTabOrder: function() {
197 this.tabIndex = UserPodTabOrder.POD_INPUT;
198 this.mainInput.tabIndex = -1;
202 * Handles keypress event (i.e. any textual input) on password input.
203 * @param {Event} e Keypress Event object.
206 handlePasswordKeyPress_: function(e) {
207 // When tabbing from the system tray a tab key press is received. Suppress
208 // this so as not to type a tab character into the password field.
209 if (e.keyCode == 9) {
216 * Top edge margin number of pixels.
220 this.style.top = cr.ui.toCssPx(top);
223 * Left edge margin number of pixels.
227 this.style.left = cr.ui.toCssPx(left);
231 * Gets signed in indicator element.
232 * @type {!HTMLDivElement}
234 get signedInIndicatorElement() {
235 return this.querySelector('.signed-in-indicator');
239 * Gets image element.
240 * @type {!HTMLImageElement}
243 return this.querySelector('.user-image');
248 * @type {!HTMLDivElement}
251 return this.querySelector('.name');
255 * Gets password field.
256 * @type {!HTMLInputElement}
258 get passwordElement() {
259 return this.querySelector('.password');
263 * Gets Caps Lock hint image.
264 * @type {!HTMLImageElement}
266 get capslockHintElement() {
267 return this.querySelector('.capslock-hint');
271 * Gets user signin button.
272 * @type {!HTMLInputElement}
274 get signinButtonElement() {
275 return this.querySelector('.signin-button');
279 * Gets action box area.
280 * @type {!HTMLInputElement}
282 get actionBoxAreaElement() {
283 return this.querySelector('.action-box-area');
287 * Gets user type icon area.
288 * @type {!HTMLInputElement}
290 get userTypeIconAreaElement() {
291 return this.querySelector('.user-type-icon-area');
295 * Gets action box menu.
296 * @type {!HTMLInputElement}
298 get actionBoxMenuElement() {
299 return this.querySelector('.action-box-menu');
303 * Gets action box menu title.
304 * @type {!HTMLInputElement}
306 get actionBoxMenuTitleElement() {
307 return this.querySelector('.action-box-menu-title');
311 * Gets action box menu title, user name item.
312 * @type {!HTMLInputElement}
314 get actionBoxMenuTitleNameElement() {
315 return this.querySelector('.action-box-menu-title-name');
319 * Gets action box menu title, user email item.
320 * @type {!HTMLInputElement}
322 get actionBoxMenuTitleEmailElement() {
323 return this.querySelector('.action-box-menu-title-email');
327 * Gets action box menu, remove user command item.
328 * @type {!HTMLInputElement}
330 get actionBoxMenuCommandElement() {
331 return this.querySelector('.action-box-menu-remove-command');
335 * Gets action box menu, remove user command item div.
336 * @type {!HTMLInputElement}
338 get actionBoxMenuRemoveElement() {
339 return this.querySelector('.action-box-menu-remove');
343 * Gets action box menu, remove user command item div.
344 * @type {!HTMLInputElement}
346 get actionBoxRemoveUserWarningElement() {
347 return this.querySelector('.action-box-remove-user-warning');
351 * Gets action box menu, remove user command item div.
352 * @type {!HTMLInputElement}
354 get actionBoxRemoveUserWarningButtonElement() {
355 return this.querySelector(
356 '.remove-warning-button');
360 * Gets the locked user indicator box.
361 * @type {!HTMLInputElement}
363 get lockedIndicatorElement() {
364 return this.querySelector('.locked-indicator');
368 * Gets the custom button. This button is normally hidden, but can be
369 * shown using the chrome.screenlockPrivate API.
370 * @type {!HTMLInputElement}
373 return this.querySelector('.custom-button');
377 * Updates the user pod element.
380 this.imageElement.src = 'chrome://userimage/' + this.user.username +
381 '?id=' + UserPod.userImageSalt_[this.user.username];
383 this.nameElement.textContent = this.user_.displayName;
384 this.signedInIndicatorElement.hidden = !this.user_.signedIn;
386 var needSignin = this.needSignin;
387 this.passwordElement.hidden = needSignin;
388 this.signinButtonElement.hidden = !needSignin;
390 this.updateActionBoxArea();
393 updateActionBoxArea: function() {
394 this.actionBoxAreaElement.hidden = this.user_.publicAccount;
395 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
397 this.actionBoxAreaElement.setAttribute(
398 'aria-label', loadTimeData.getStringF(
399 'podMenuButtonAccessibleName', this.user_.emailAddress));
400 this.actionBoxMenuRemoveElement.setAttribute(
401 'aria-label', loadTimeData.getString(
402 'podMenuRemoveItemAccessibleName'));
403 this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
404 loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
405 this.user_.displayName;
406 this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
407 this.actionBoxMenuTitleEmailElement.hidden =
408 this.user_.locallyManagedUser;
410 this.actionBoxMenuCommandElement.textContent =
411 loadTimeData.getString('removeUser');
412 this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
413 'passwordFieldAccessibleName', this.user_.emailAddress));
414 this.userTypeIconAreaElement.hidden = !this.user_.locallyManagedUser;
418 * The user that this pod represents.
426 this.user_ = userDict;
431 * Whether signin is required for this user.
434 // Signin is performed if the user has an invalid oauth token and is
435 // not currently signed in (i.e. not the lock screen).
436 return this.user.oauthTokenStatus != OAuthTokenStatus.VALID_OLD &&
437 this.user.oauthTokenStatus != OAuthTokenStatus.VALID_NEW &&
442 * Gets main input element.
443 * @type {(HTMLButtonElement|HTMLInputElement)}
446 if (!this.signinButtonElement.hidden)
447 return this.signinButtonElement;
449 return this.passwordElement;
453 * Whether action box button is in active state.
456 get isActionBoxMenuActive() {
457 return this.actionBoxAreaElement.classList.contains('active');
459 set isActionBoxMenuActive(active) {
460 if (active == this.isActionBoxMenuActive)
464 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
465 if (this.actionBoxRemoveUserWarningElement)
466 this.actionBoxRemoveUserWarningElement.hidden = true;
468 // Clear focus first if another pod is focused.
469 if (!this.parentNode.isFocused(this)) {
470 this.parentNode.focusPod(undefined, true);
471 this.actionBoxAreaElement.focus();
473 this.actionBoxAreaElement.classList.add('active');
475 this.actionBoxAreaElement.classList.remove('active');
480 * Whether action box button is in hovered state.
483 get isActionBoxMenuHovered() {
484 return this.actionBoxAreaElement.classList.contains('hovered');
486 set isActionBoxMenuHovered(hovered) {
487 if (hovered == this.isActionBoxMenuHovered)
491 this.actionBoxAreaElement.classList.add('hovered');
492 this.classList.add('hovered');
494 this.actionBoxAreaElement.classList.remove('hovered');
495 this.classList.remove('hovered');
500 * Updates the image element of the user.
502 updateUserImage: function() {
503 UserPod.userImageSalt_[this.user.username] = new Date().getTime();
508 * Focuses on input element.
510 focusInput: function() {
511 var needSignin = this.needSignin;
512 this.signinButtonElement.hidden = !needSignin;
513 this.passwordElement.hidden = needSignin;
515 // Move tabIndex from the whole pod to the main input.
517 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
518 this.mainInput.focus();
523 * @return {boolean} True if activated successfully.
525 activate: function() {
526 if (!this.signinButtonElement.hidden) {
528 } else if (!this.passwordElement.value) {
531 Oobe.disableSigninUI();
532 chrome.send('authenticateUser',
533 [this.user.username, this.passwordElement.value]);
539 showSupervisedUserSigninWarning: function() {
540 // Locally managed user token has been invalidated.
541 // Make sure that pod is focused i.e. "Sign in" button is seen.
542 this.parentNode.focusPod(this);
544 var error = document.createElement('div');
545 var messageDiv = document.createElement('div');
546 messageDiv.className = 'error-message-bubble';
547 messageDiv.textContent =
548 loadTimeData.getString('supervisedUserExpiredTokenWarning');
549 error.appendChild(messageDiv);
551 $('bubble').showContentForElement(
552 this.signinButtonElement,
553 cr.ui.Bubble.Attachment.TOP,
555 this.signinButtonElement.offsetWidth / 2,
560 * Shows signin UI for this user.
562 showSigninUI: function() {
563 if (this.user.locallyManagedUser) {
564 this.showSupervisedUserSigninWarning();
566 this.parentNode.showSigninUI(this.user.emailAddress);
571 * Resets the input field and updates the tab order of pod controls.
572 * @param {boolean} takeFocus If true, input field takes focus.
574 reset: function(takeFocus) {
575 this.passwordElement.value = '';
577 this.focusInput(); // This will set a custom tab order.
579 this.resetTabOrder();
583 * Handles a click event on action area button.
584 * @param {Event} e Click event.
586 handleActionAreaButtonClick_: function(e) {
587 if (this.parentNode.disabled)
589 this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
593 * Handles a keydown event on action area button.
594 * @param {Event} e KeyDown event.
596 handleActionAreaButtonKeyDown_: function(e) {
599 switch (e.keyIdentifier) {
601 case 'U+0020': // Space
602 if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
603 this.isActionBoxMenuActive = true;
608 if (this.isActionBoxMenuActive) {
609 this.actionBoxMenuRemoveElement.tabIndex =
610 UserPodTabOrder.PAD_MENU_ITEM;
611 this.actionBoxMenuRemoveElement.focus();
615 case 'U+001B': // Esc
616 this.isActionBoxMenuActive = false;
619 case 'U+0009': // Tab
620 this.parentNode.focusPod();
622 this.isActionBoxMenuActive = false;
628 * Handles a click event on remove user command.
629 * @param {Event} e Click event.
631 handleRemoveCommandClick_: function(e) {
632 if (this.user.locallyManagedUser || this.user.isDesktopUser) {
633 this.showRemoveWarning_();
636 if (this.isActionBoxMenuActive)
637 chrome.send('removeUser', [this.user.username]);
641 * Shows remove warning for managed users.
643 showRemoveWarning_: function() {
644 this.actionBoxMenuRemoveElement.hidden = true;
645 this.actionBoxRemoveUserWarningElement.hidden = false;
649 * Handles a click event on remove user confirmation button.
650 * @param {Event} e Click event.
652 handleRemoveUserConfirmationClick_: function(e) {
653 if (this.isActionBoxMenuActive)
654 chrome.send('removeUser', [this.user.username]);
658 * Handles a keydown event on remove command.
659 * @param {Event} e KeyDown event.
661 handleRemoveCommandKeyDown_: function(e) {
664 switch (e.keyIdentifier) {
666 chrome.send('removeUser', [this.user.username]);
673 case 'U+001B': // Esc
674 this.actionBoxAreaElement.focus();
675 this.isActionBoxMenuActive = false;
679 this.actionBoxAreaElement.focus();
680 this.isActionBoxMenuActive = false;
686 * Handles a blur event on remove command.
687 * @param {Event} e Blur event.
689 handleRemoveCommandBlur_: function(e) {
692 this.actionBoxMenuRemoveElement.tabIndex = -1;
696 * Handles mousedown event on a user pod.
697 * @param {Event} e Mousedown event.
699 handleMouseDown_: function(e) {
700 if (this.parentNode.disabled)
703 if (!this.signinButtonElement.hidden && !this.isActionBoxMenuActive) {
705 // Prevent default so that we don't trigger 'focus' event.
711 * Called when the custom button is clicked.
713 handleCustomButtonClick_: function() {
714 chrome.send('customButtonClicked', [this.user.username]);
719 * Creates a public account user pod.
723 var PublicAccountUserPod = cr.ui.define(function() {
724 var node = UserPod();
726 var extras = $('public-account-user-pod-extras-template').children;
727 for (var i = 0; i < extras.length; ++i) {
728 var el = extras[i].cloneNode(true);
729 node.appendChild(el);
735 PublicAccountUserPod.prototype = {
736 __proto__: UserPod.prototype,
739 * "Enter" button in expanded side pane.
740 * @type {!HTMLButtonElement}
742 get enterButtonElement() {
743 return this.querySelector('.enter-button');
747 * Boolean flag of whether the pod is showing the side pane. The flag
748 * controls whether 'expanded' class is added to the pod's class list and
749 * resets tab order because main input element changes when the 'expanded'
754 return this.classList.contains('expanded');
756 set expanded(expanded) {
757 if (this.expanded == expanded)
760 this.resetTabOrder();
761 this.classList.toggle('expanded', expanded);
764 this.classList.add('animating');
765 this.addEventListener('webkitTransitionEnd', function f(e) {
766 self.removeEventListener('webkitTransitionEnd', f);
767 self.classList.remove('animating');
769 // Accessibility focus indicator does not move with the focused
770 // element. Sends a 'focus' event on the currently focused element
771 // so that accessibility focus indicator updates its location.
772 if (document.activeElement)
773 document.activeElement.dispatchEvent(new Event('focus'));
785 return this.enterButtonElement;
787 return this.nameElement;
791 decorate: function() {
792 UserPod.prototype.decorate.call(this);
794 this.classList.remove('need-password');
795 this.classList.add('public-account');
797 this.nameElement.addEventListener('keydown', (function(e) {
798 if (e.keyIdentifier == 'Enter') {
799 this.parentNode.activatedPod = this;
800 // Stop this keydown event from bubbling up to PodRow handler.
802 // Prevent default so that we don't trigger a 'click' event on the
803 // newly focused "Enter" button.
808 var learnMore = this.querySelector('.learn-more');
809 learnMore.addEventListener('mousedown', stopEventPropagation);
810 learnMore.addEventListener('click', this.handleLearnMoreEvent);
811 learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
813 learnMore = this.querySelector('.side-pane-learn-more');
814 learnMore.addEventListener('click', this.handleLearnMoreEvent);
815 learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
817 this.enterButtonElement.addEventListener('click', (function(e) {
818 this.enterButtonElement.disabled = true;
819 chrome.send('launchPublicAccount', [this.user.username]);
824 * Updates the user pod element.
827 UserPod.prototype.update.call(this);
828 this.querySelector('.side-pane-name').textContent =
829 this.user_.displayName;
830 this.querySelector('.info').textContent =
831 loadTimeData.getStringF('publicAccountInfoFormat',
832 this.user_.enterpriseDomain);
836 focusInput: function() {
837 // Move tabIndex from the whole pod to the main input.
839 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
840 this.mainInput.focus();
844 reset: function(takeFocus) {
846 this.expanded = false;
847 this.enterButtonElement.disabled = false;
848 UserPod.prototype.reset.call(this, takeFocus);
852 activate: function() {
853 this.expanded = true;
859 handleMouseDown_: function(e) {
860 if (this.parentNode.disabled)
863 this.parentNode.focusPod(this);
864 this.parentNode.activatedPod = this;
865 // Prevent default so that we don't trigger 'focus' event.
870 * Handle mouse and keyboard events for the learn more button.
871 * Triggering the button causes information about public sessions to be
873 * @param {Event} event Mouse or keyboard event.
875 handleLearnMoreEvent: function(event) {
876 switch (event.type) {
877 // Show informaton on left click. Let any other clicks propagate.
879 if (event.button != 0)
882 // Show informaton when <Return> or <Space> is pressed. Let any other
883 // key presses propagate.
885 switch (event.keyCode) {
894 chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
895 stopEventPropagation(event);
900 * Creates a user pod to be used only in desktop chrome.
904 var DesktopUserPod = cr.ui.define(function() {
905 // Don't just instantiate a UserPod(), as this will call decorate() on the
906 // parent object, and add duplicate event listeners.
907 var node = $('user-pod-template').cloneNode(true);
908 node.removeAttribute('id');
912 DesktopUserPod.prototype = {
913 __proto__: UserPod.prototype,
917 if (!this.passwordElement.hidden)
918 return this.passwordElement;
920 return this.nameElement;
924 decorate: function() {
925 UserPod.prototype.decorate.call(this);
930 // TODO(noms): Use the actual profile avatar for local profiles once the
931 // new, non-pixellated avatars are available.
932 this.imageElement.src = this.user.emailAddress == '' ?
933 'chrome://theme/IDR_USER_MANAGER_DEFAULT_AVATAR' :
935 this.nameElement.textContent = this.user.displayName;
937 var isLockedUser = this.user.needsSignin;
938 this.signinButtonElement.hidden = true;
939 this.lockedIndicatorElement.hidden = !isLockedUser;
940 this.passwordElement.hidden = !isLockedUser;
941 this.nameElement.hidden = isLockedUser;
943 UserPod.prototype.updateActionBoxArea.call(this);
947 focusInput: function() {
948 // For focused pods, display the name unless the pod is locked.
949 var isLockedUser = this.user.needsSignin;
950 this.signinButtonElement.hidden = true;
951 this.lockedIndicatorElement.hidden = !isLockedUser;
952 this.passwordElement.hidden = !isLockedUser;
953 this.nameElement.hidden = isLockedUser;
955 // Move tabIndex from the whole pod to the main input.
957 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
958 this.mainInput.focus();
962 reset: function(takeFocus) {
963 // Always display the user's name for unfocused pods.
965 this.nameElement.hidden = false;
966 UserPod.prototype.reset.call(this, takeFocus);
970 activate: function() {
971 if (this.passwordElement.hidden) {
972 Oobe.launchUser(this.user.emailAddress, this.user.displayName);
973 } else if (!this.passwordElement.value) {
976 chrome.send('authenticatedLaunchUser',
977 [this.user.emailAddress,
978 this.user.displayName,
979 this.passwordElement.value]);
981 this.passwordElement.value = '';
986 handleMouseDown_: function(e) {
987 if (this.parentNode.disabled)
991 this.parentNode.lastFocusedPod_ = this;
993 // If this is an unlocked pod, then open a browser window. Otherwise
994 // just activate the pod and show the password field.
995 if (!this.user.needsSignin && !this.isActionBoxMenuActive)
1000 handleRemoveUserConfirmationClick_: function(e) {
1001 chrome.send('removeUser', [this.user.profilePath]);
1006 * Creates a new pod row element.
1008 * @extends {HTMLDivElement}
1010 var PodRow = cr.ui.define('podrow');
1012 PodRow.prototype = {
1013 __proto__: HTMLDivElement.prototype,
1015 // Whether this user pod row is shown for the first time.
1018 // True if inside focusPod().
1019 insideFocusPod_: false,
1022 focusedPod_: undefined,
1024 // Activated pod, i.e. the pod of current login attempt.
1025 activatedPod_: undefined,
1027 // Pod that was most recently focused, if any.
1028 lastFocusedPod_: undefined,
1030 // Note: created only in decorate() !
1031 wallpaperLoader_: undefined,
1033 // Pods whose initial images haven't been loaded yet.
1034 podsWithPendingImages_: [],
1037 decorate: function() {
1038 // Event listeners that are installed for the time period during which
1039 // the element is visible.
1041 focus: [this.handleFocus_.bind(this), true /* useCapture */],
1042 click: [this.handleClick_.bind(this), true],
1043 mousemove: [this.handleMouseMove_.bind(this), false],
1044 keydown: [this.handleKeyDown.bind(this), false]
1046 this.wallpaperLoader_ = new login.WallpaperLoader();
1050 * Returns all the pods in this pod row.
1054 return Array.prototype.slice.call(this.children);
1058 * Return true if user pod row has only single user pod in it.
1062 return this.children.length == 1;
1066 * Returns pod with the given username (null if there is no such pod).
1067 * @param {string} username Username to be matched.
1068 * @return {Object} Pod with the given username. null if pod hasn't been
1071 getPodWithUsername_: function(username) {
1072 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1073 if (pod.user.username == username)
1080 * True if the the pod row is disabled (handles no user interaction).
1085 return this.disabled_;
1087 set disabled(value) {
1088 this.disabled_ = value;
1089 var controls = this.querySelectorAll('button,input');
1090 for (var i = 0, control; control = controls[i]; ++i) {
1091 control.disabled = value;
1096 * Creates a user pod from given email.
1097 * @param {string} email User's email.
1099 createUserPod: function(user) {
1101 if (user.isDesktopUser)
1102 userPod = new DesktopUserPod({user: user});
1103 else if (user.publicAccount)
1104 userPod = new PublicAccountUserPod({user: user});
1106 userPod = new UserPod({user: user});
1108 userPod.hidden = false;
1113 * Add an existing user pod to this pod row.
1114 * @param {!Object} user User info dictionary.
1115 * @param {boolean} animated Whether to use init animation.
1117 addUserPod: function(user, animated) {
1118 var userPod = this.createUserPod(user);
1120 userPod.classList.add('init');
1121 userPod.nameElement.classList.add('init');
1124 this.appendChild(userPod);
1125 userPod.initialize();
1129 * Removes user pod from pod row.
1130 * @param {string} email User's email.
1132 removeUserPod: function(username) {
1133 var podToRemove = this.getPodWithUsername_(username);
1134 if (podToRemove == null) {
1135 console.warn('Attempt to remove not existing pod for ' + username +
1139 this.removeChild(podToRemove);
1144 * Returns index of given pod or -1 if not found.
1145 * @param {UserPod} pod Pod to look up.
1148 indexOf_: function(pod) {
1149 for (var i = 0; i < this.pods.length; ++i) {
1150 if (pod == this.pods[i])
1157 * Start first time show animation.
1159 startInitAnimation: function() {
1160 // Schedule init animation.
1161 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1162 window.setTimeout(removeClass, 500 + i * 70, pod, 'init');
1163 window.setTimeout(removeClass, 700 + i * 70, pod.nameElement, 'init');
1168 * Start login success animation.
1170 startAuthenticatedAnimation: function() {
1171 var activated = this.indexOf_(this.activatedPod_);
1172 if (activated == -1)
1175 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1177 pod.classList.add('left');
1178 else if (i > activated)
1179 pod.classList.add('right');
1181 pod.classList.add('zoom');
1186 * Populates pod row with given existing users and start init animation.
1187 * @param {array} users Array of existing user emails.
1188 * @param {boolean} animated Whether to use init animation.
1190 loadPods: function(users, animated) {
1191 // Clear existing pods.
1192 this.innerHTML = '';
1193 this.focusedPod_ = undefined;
1194 this.activatedPod_ = undefined;
1195 this.lastFocusedPod_ = undefined;
1197 // Switch off animation
1198 Oobe.getInstance().toggleClass('flying-pods', false);
1200 // Populate the pod row.
1201 for (var i = 0; i < users.length; ++i) {
1202 this.addUserPod(users[i], animated);
1204 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1205 this.podsWithPendingImages_.push(pod);
1207 // Make sure we eventually show the pod row, even if some image is stuck.
1208 setTimeout(function() {
1209 $('pod-row').classList.remove('images-loading');
1210 }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
1214 // Without timeout changes in pods positions will be animated even though
1215 // it happened when 'flying-pods' class was disabled.
1216 setTimeout(function() {
1217 Oobe.getInstance().toggleClass('flying-pods', true);
1220 this.focusPod(this.preselectedPod);
1224 * Shows a button on a user pod with an icon. Clicking on this button
1225 * triggers an event used by the chrome.screenlockPrivate API.
1226 * @param {string} username Username of pod to add button
1227 * @param {string} iconURL URL of the button icon
1229 showUserPodButton: function(username, iconURL) {
1230 var pod = this.getPodWithUsername_(username);
1232 console.error('Unable to show user pod button for ' + username +
1233 ': user pod not found.');
1237 pod.customButton.hidden = false;
1239 pod.customButton.querySelector('.custom-button-icon');
1244 * Called when window was resized.
1246 onWindowResize: function() {
1247 var layout = this.calculateLayout_();
1248 if (layout.columns != this.columns || layout.rows != this.rows)
1253 * Returns width of podrow having |columns| number of columns.
1256 columnsToWidth_: function(columns) {
1257 var margin = MARGIN_BY_COLUMNS[columns];
1258 return 2 * POD_ROW_PADDING + columns * POD_WIDTH + (columns - 1) * margin;
1262 * Returns height of podrow having |rows| number of rows.
1265 rowsToHeight_: function(rows) {
1266 return 2 * POD_ROW_PADDING + rows * POD_HEIGHT;
1270 * Calculates number of columns and rows that podrow should have in order to
1271 * hold as much its pods as possible for current screen size. Also it tries
1272 * to choose layout that looks good.
1273 * @return {{columns: number, rows: number}}
1275 calculateLayout_: function() {
1276 var preferredColumns = this.pods.length < COLUMNS.length ?
1277 COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
1278 var maxWidth = Oobe.getInstance().clientAreaSize.width;
1279 var columns = preferredColumns;
1280 while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
1282 var rows = Math.floor((this.pods.length - 1) / columns) + 1;
1283 var maxHeigth = Oobe.getInstance().clientAreaSize.height;
1284 while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
1286 // One more iteration if it's not enough cells to place all pods.
1287 while (maxWidth >= this.columnsToWidth_(columns + 1) &&
1288 columns * rows < this.pods.length &&
1289 columns < MAX_NUMBER_OF_COLUMNS) {
1292 return {columns: columns, rows: rows};
1296 * Places pods onto their positions onto pod grid.
1299 placePods_: function() {
1300 var layout = this.calculateLayout_();
1301 var columns = this.columns = layout.columns;
1302 var rows = this.rows = layout.rows;
1303 var maxPodsNumber = columns * rows;
1304 var margin = MARGIN_BY_COLUMNS[columns];
1305 this.parentNode.setPreferredSize(
1306 this.columnsToWidth_(columns), this.rowsToHeight_(rows));
1307 this.pods.forEach(function(pod, index) {
1308 if (pod.offsetHeight != POD_HEIGHT)
1309 console.error('Pod offsetHeight and POD_HEIGHT are not equal.');
1310 if (pod.offsetWidth != POD_WIDTH)
1311 console.error('Pod offsetWidht and POD_WIDTH are not equal.');
1312 if (index >= maxPodsNumber) {
1317 var column = index % columns;
1318 var row = Math.floor(index / columns);
1319 pod.left = POD_ROW_PADDING + column * (POD_WIDTH + margin);
1320 pod.top = POD_ROW_PADDING + row * POD_HEIGHT;
1322 Oobe.getInstance().updateScreenSize(this.parentNode);
1326 * Number of columns.
1329 set columns(columns) {
1330 // Cannot use 'columns' here.
1331 this.setAttribute('ncolumns', columns);
1334 return this.getAttribute('ncolumns');
1342 // Cannot use 'rows' here.
1343 this.setAttribute('nrows', rows);
1346 return this.getAttribute('nrows');
1350 * Whether the pod is currently focused.
1351 * @param {UserPod} pod Pod to check for focus.
1352 * @return {boolean} Pod focus status.
1354 isFocused: function(pod) {
1355 return this.focusedPod_ == pod;
1359 * Focuses a given user pod or clear focus when given null.
1360 * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
1361 * @param {boolean=} opt_force If true, forces focus update even when
1362 * podToFocus is already focused.
1364 focusPod: function(podToFocus, opt_force) {
1365 if (this.isFocused(podToFocus) && !opt_force) {
1366 this.keyboardActivated_ = false;
1370 // Make sure there's only one focusPod operation happening at a time.
1371 if (this.insideFocusPod_) {
1372 this.keyboardActivated_ = false;
1375 this.insideFocusPod_ = true;
1377 this.wallpaperLoader_.reset();
1378 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1379 if (!this.isSinglePod) {
1380 pod.isActionBoxMenuActive = false;
1382 if (pod != podToFocus) {
1383 pod.isActionBoxMenuHovered = false;
1384 pod.classList.remove('focused');
1385 pod.classList.remove('faded');
1390 // Clear any error messages for previous pod.
1391 if (!this.isFocused(podToFocus))
1394 var hadFocus = !!this.focusedPod_;
1395 this.focusedPod_ = podToFocus;
1397 podToFocus.classList.remove('faded');
1398 podToFocus.classList.add('focused');
1399 podToFocus.reset(true); // Reset and give focus.
1400 chrome.send('focusPod', [podToFocus.user.username]);
1402 this.wallpaperLoader_.scheduleLoad(podToFocus.user.username);
1403 this.firstShown_ = false;
1404 this.lastFocusedPod_ = podToFocus;
1406 this.insideFocusPod_ = false;
1407 this.keyboardActivated_ = false;
1411 * Focuses a given user pod by index or clear focus when given null.
1412 * @param {int=} podToFocus index of User pod to focus.
1413 * @param {boolean=} opt_force If true, forces focus update even when
1414 * podToFocus is already focused.
1416 focusPodByIndex: function(podToFocus, opt_force) {
1417 if (podToFocus < this.pods.length)
1418 this.focusPod(this.pods[podToFocus], opt_force);
1422 * Resets wallpaper to the last active user's wallpaper, if any.
1424 loadLastWallpaper: function() {
1425 if (this.lastFocusedPod_)
1426 this.wallpaperLoader_.scheduleLoad(this.lastFocusedPod_.user.username);
1430 * Handles 'onWallpaperLoaded' event. Recalculates statistics and
1431 * [re]schedules next wallpaper load.
1433 onWallpaperLoaded: function(username) {
1434 this.wallpaperLoader_.onWallpaperLoaded(username);
1438 * Returns the currently activated pod.
1441 get activatedPod() {
1442 return this.activatedPod_;
1444 set activatedPod(pod) {
1445 if (pod && pod.activate())
1446 this.activatedPod_ = pod;
1450 * The pod of the signed-in user, if any; null otherwise.
1454 for (var i = 0, pod; pod = this.pods[i]; ++i) {
1455 if (pod.user.signedIn)
1462 * The pod that is preselected on user pod row show.
1465 get preselectedPod() {
1466 var lockedPod = this.lockedPod;
1467 var preselectedPod = PRESELECT_FIRST_POD ?
1468 lockedPod || this.pods[0] : lockedPod;
1469 return preselectedPod;
1474 * @param {boolean} takeFocus True to take focus.
1476 reset: function(takeFocus) {
1477 this.disabled = false;
1478 if (this.activatedPod_)
1479 this.activatedPod_.reset(takeFocus);
1483 * Restores input focus to current selected pod, if there is any.
1485 refocusCurrentPod: function() {
1486 if (this.focusedPod_) {
1487 this.focusedPod_.focusInput();
1492 * Clears focused pod password field.
1494 clearFocusedPod: function() {
1495 if (!this.disabled && this.focusedPod_)
1496 this.focusedPod_.reset(true);
1501 * @param {string} email Email for signin UI.
1503 showSigninUI: function(email) {
1504 // Clear any error messages that might still be around.
1506 this.disabled = true;
1507 this.lastFocusedPod_ = this.getPodWithUsername_(email);
1508 Oobe.showSigninUI(email);
1512 * Updates current image of a user.
1513 * @param {string} username User for which to update the image.
1515 updateUserImage: function(username) {
1516 var pod = this.getPodWithUsername_(username);
1518 pod.updateUserImage();
1522 * Resets OAuth token status (invalidates it).
1523 * @param {string} username User for which to reset the status.
1525 resetUserOAuthTokenStatus: function(username) {
1526 var pod = this.getPodWithUsername_(username);
1528 pod.user.oauthTokenStatus = OAuthTokenStatus.INVALID_OLD;
1531 console.log('Failed to update Gaia state for: ' + username);
1536 * Handler of click event.
1537 * @param {Event} e Click Event object.
1540 handleClick_: function(e) {
1544 // Clear all menus if the click is outside pod menu and its
1546 if (!findAncestorByClass(e.target, 'action-box-menu') &&
1547 !findAncestorByClass(e.target, 'action-box-area')) {
1548 for (var i = 0, pod; pod = this.pods[i]; ++i)
1549 pod.isActionBoxMenuActive = false;
1552 // Clears focus if not clicked on a pod and if there's more than one pod.
1553 var pod = findAncestorByClass(e.target, 'pod');
1554 if ((!pod || pod.parentNode != this) && !this.isSinglePod) {
1559 pod.isActionBoxMenuHovered = true;
1561 // Return focus back to single pod.
1562 if (this.isSinglePod) {
1563 this.focusPod(this.focusedPod_, true /* force */);
1565 this.focusedPod_.isActionBoxMenuHovered = false;
1568 // Also stop event propagation.
1569 if (pod && e.target == pod.imageElement)
1570 e.stopPropagation();
1574 * Handler of mouse move event.
1575 * @param {Event} e Click Event object.
1578 handleMouseMove_: function(e) {
1581 if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
1584 // Defocus (thus hide) action box, if it is focused on a user pod
1585 // and the pointer is not hovering over it.
1586 var pod = findAncestorByClass(e.target, 'pod');
1587 if (document.activeElement &&
1588 document.activeElement.parentNode != pod &&
1589 document.activeElement.classList.contains('action-box-area')) {
1590 document.activeElement.parentNode.focus();
1594 pod.isActionBoxMenuHovered = true;
1596 // Hide action boxes on other user pods.
1597 for (var i = 0, p; p = this.pods[i]; ++i)
1598 if (p != pod && !p.isActionBoxMenuActive)
1599 p.isActionBoxMenuHovered = false;
1603 * Handles focus event.
1604 * @param {Event} e Focus Event object.
1607 handleFocus_: function(e) {
1610 if (e.target.parentNode == this) {
1612 if (e.target.classList.contains('focused'))
1613 e.target.focusInput();
1615 this.focusPod(e.target);
1619 var pod = findAncestorByClass(e.target, 'pod');
1620 if (pod && pod.parentNode == this) {
1621 // Focus on a control of a pod but not on the action area button.
1622 if (!pod.classList.contains('focused') &&
1623 !e.target.classList.contains('action-box-button')) {
1630 // Clears pod focus when we reach here. It means new focus is neither
1631 // on a pod nor on a button/input for a pod.
1632 // Do not "defocus" user pod when it is a single pod.
1633 // That means that 'focused' class will not be removed and
1634 // input field/button will always be visible.
1635 if (!this.isSinglePod)
1640 * Handler of keydown event.
1641 * @param {Event} e KeyDown Event object.
1643 handleKeyDown: function(e) {
1646 var editing = e.target.tagName == 'INPUT' && e.target.value;
1647 switch (e.keyIdentifier) {
1650 this.keyboardActivated_ = true;
1651 if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
1652 this.focusPod(this.focusedPod_.previousElementSibling);
1654 this.focusPod(this.lastElementChild);
1656 e.stopPropagation();
1661 this.keyboardActivated_ = true;
1662 if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
1663 this.focusPod(this.focusedPod_.nextElementSibling);
1665 this.focusPod(this.firstElementChild);
1667 e.stopPropagation();
1671 if (this.focusedPod_) {
1672 this.activatedPod = this.focusedPod_;
1673 e.stopPropagation();
1676 case 'U+001B': // Esc
1677 if (!this.isSinglePod)
1684 * Called right after the pod row is shown.
1686 handleAfterShow: function() {
1687 // Without timeout changes in pods positions will be animated even though
1688 // it happened when 'flying-pods' class was disabled.
1689 setTimeout(function() {
1690 Oobe.getInstance().toggleClass('flying-pods', true);
1692 // Force input focus for user pod on show and once transition ends.
1693 if (this.focusedPod_) {
1694 var focusedPod = this.focusedPod_;
1695 var screen = this.parentNode;
1697 focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
1698 if (e.target == focusedPod) {
1699 focusedPod.removeEventListener('webkitTransitionEnd', f);
1700 focusedPod.reset(true);
1701 // Notify screen that it is ready.
1703 self.wallpaperLoader_.scheduleLoad(focusedPod.user.username);
1706 // Guard timer for 1 second -- it would conver all possible animations.
1707 ensureTransitionEndEvent(focusedPod, 1000);
1712 * Called right before the pod row is shown.
1714 handleBeforeShow: function() {
1715 Oobe.getInstance().toggleClass('flying-pods', false);
1716 for (var event in this.listeners_) {
1717 this.ownerDocument.addEventListener(
1718 event, this.listeners_[event][0], this.listeners_[event][1]);
1720 $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
1724 * Called when the element is hidden.
1726 handleHide: function() {
1727 for (var event in this.listeners_) {
1728 this.ownerDocument.removeEventListener(
1729 event, this.listeners_[event][0], this.listeners_[event][1]);
1731 $('login-header-bar').buttonsTabIndex = 0;
1735 * Called when a pod's user image finishes loading.
1737 handlePodImageLoad: function(pod) {
1738 var index = this.podsWithPendingImages_.indexOf(pod);
1743 this.podsWithPendingImages_.splice(index, 1);
1744 if (this.podsWithPendingImages_.length == 0) {
1745 this.classList.remove('images-loading');