MacViews: Get c/b/ui/views/tabs to build on Mac
[chromium-blink-merge.git] / ui / login / account_picker / user_pod_row.js
blobdbac7f3deafd7ca5546ca69ac05587e41b7cf7d0
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.
5 /**
6 * @fileoverview User pod row implementation.
7 */
9 cr.define('login', function() {
10 /**
11 * Number of displayed columns depending on user pod count.
12 * @type {Array.<number>}
13 * @const
15 var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6];
17 /**
18 * Mapping between number of columns in pod-row and margin between user pods
19 * for such layout.
20 * @type {Array.<number>}
21 * @const
23 var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12];
25 /**
26 * Mapping between number of columns in the desktop pod-row and margin
27 * between user pods for such layout.
28 * @type {Array.<number>}
29 * @const
31 var DESKTOP_MARGIN_BY_COLUMNS = [undefined, 15, 15, 15, 15, 15, 15];
33 /**
34 * Maximal number of columns currently supported by pod-row.
35 * @type {number}
36 * @const
38 var MAX_NUMBER_OF_COLUMNS = 6;
40 /**
41 * Maximal number of rows if sign-in banner is displayed alonside.
42 * @type {number}
43 * @const
45 var MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER = 2;
47 /**
48 * Variables used for pod placement processing. Width and height should be
49 * synced with computed CSS sizes of pods.
51 var POD_WIDTH = 180;
52 var PUBLIC_EXPANDED_BASIC_WIDTH = 500;
53 var PUBLIC_EXPANDED_ADVANCED_WIDTH = 610;
54 var CROS_POD_HEIGHT = 213;
55 var DESKTOP_POD_HEIGHT = 226;
56 var POD_ROW_PADDING = 10;
57 var DESKTOP_ROW_PADDING = 15;
58 var CUSTOM_ICON_CONTAINER_SIZE = 40;
60 /**
61 * Minimal padding between user pod and virtual keyboard.
62 * @type {number}
63 * @const
65 var USER_POD_KEYBOARD_MIN_PADDING = 20;
67 /**
68 * Maximum time for which the pod row remains hidden until all user images
69 * have been loaded.
70 * @type {number}
71 * @const
73 var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
75 /**
76 * Public session help topic identifier.
77 * @type {number}
78 * @const
80 var HELP_TOPIC_PUBLIC_SESSION = 3041033;
82 /**
83 * Tab order for user pods. Update these when adding new controls.
84 * @enum {number}
85 * @const
87 var UserPodTabOrder = {
88 POD_INPUT: 1, // Password input fields (and whole pods themselves).
89 POD_CUSTOM_ICON: 2, // Pod custom icon next to passwrod input field.
90 HEADER_BAR: 3, // Buttons on the header bar (Shutdown, Add User).
91 ACTION_BOX: 4, // Action box buttons.
92 PAD_MENU_ITEM: 5 // User pad menu items (Remove this user).
95 /**
96 * Supported authentication types. Keep in sync with the enum in
97 * chrome/browser/signin/screenlock_bridge.h
98 * @enum {number}
99 * @const
101 var AUTH_TYPE = {
102 OFFLINE_PASSWORD: 0,
103 ONLINE_SIGN_IN: 1,
104 NUMERIC_PIN: 2,
105 USER_CLICK: 3,
106 EXPAND_THEN_USER_CLICK: 4,
107 FORCE_OFFLINE_PASSWORD: 5
111 * Names of authentication types.
113 var AUTH_TYPE_NAMES = {
114 0: 'offlinePassword',
115 1: 'onlineSignIn',
116 2: 'numericPin',
117 3: 'userClick',
118 4: 'expandThenUserClick',
119 5: 'forceOfflinePassword'
122 // Focus and tab order are organized as follows:
124 // (1) all user pods have tab index 1 so they are traversed first;
125 // (2) when a user pod is activated, its tab index is set to -1 and its
126 // main input field gets focus and tab index 1;
127 // (3) if user pod custom icon is interactive, it has tab index 2 so it
128 // follows the input.
129 // (4) buttons on the header bar have tab index 3 so they follow the custom
130 // icon, or user pod if custom icon is not interactive;
131 // (5) Action box buttons have tab index 4 and follow header bar buttons;
132 // (6) lastly, focus jumps to the Status Area and back to user pods.
134 // 'Focus' event is handled by a capture handler for the whole document
135 // and in some cases 'mousedown' event handlers are used instead of 'click'
136 // handlers where it's necessary to prevent 'focus' event from being fired.
139 * Helper function to remove a class from given element.
140 * @param {!HTMLElement} el Element whose class list to change.
141 * @param {string} cl Class to remove.
143 function removeClass(el, cl) {
144 el.classList.remove(cl);
148 * Creates a user pod.
149 * @constructor
150 * @extends {HTMLDivElement}
152 var UserPod = cr.ui.define(function() {
153 var node = $('user-pod-template').cloneNode(true);
154 node.removeAttribute('id');
155 return node;
159 * Stops event propagation from the any user pod child element.
160 * @param {Event} e Event to handle.
162 function stopEventPropagation(e) {
163 // Prevent default so that we don't trigger a 'focus' event.
164 e.preventDefault();
165 e.stopPropagation();
169 * Creates an element for custom icon shown in a user pod next to the input
170 * field.
171 * @constructor
172 * @extends {HTMLDivElement}
174 var UserPodCustomIcon = cr.ui.define(function() {
175 var node = document.createElement('div');
176 node.classList.add('custom-icon-container');
177 node.hidden = true;
179 // Create the actual icon element and add it as a child to the container.
180 var iconNode = document.createElement('div');
181 iconNode.classList.add('custom-icon');
182 node.appendChild(iconNode);
183 return node;
187 * The supported user pod custom icons.
188 * {@code id} properties should be in sync with values set by C++ side.
189 * {@code class} properties are CSS classes used to set the icons' background.
190 * @const {Array.<{id: !string, class: !string}>}
192 UserPodCustomIcon.ICONS = [
193 {id: 'locked', class: 'custom-icon-locked'},
194 {id: 'unlocked', class: 'custom-icon-unlocked'},
195 {id: 'hardlocked', class: 'custom-icon-hardlocked'},
196 {id: 'spinner', class: 'custom-icon-spinner'}
199 UserPodCustomIcon.prototype = {
200 __proto__: HTMLDivElement.prototype,
203 * Tooltip to be shown when the user hovers over the icon. The icon
204 * properties may be set so the tooltip is shown automatically when the icon
205 * is updated. The tooltip is shown in a bubble attached to the icon
206 * element.
207 * @type {string}
208 * @private
210 tooltip_: '',
213 * Whether the tooltip is shown and the user is hovering over the icon.
214 * @type {boolean}
215 * @private
217 tooltipActive_: false,
220 * Whether the icon has been shown as a result of |autoshow| parameter begin
221 * set rather than user hovering over the icon.
222 * If this is set, the tooltip will not be removed when the mouse leaves the
223 * icon.
224 * @type {boolean}
225 * @private
227 tooltipAutoshown_: false,
230 * A reference to the timeout for showing tooltip after mouse enters the
231 * icon.
232 * @type {?number}
233 * @private
235 showTooltipTimeout_: null,
238 * When {@code fadeOut} is called, the element gets hidden using fadeout
239 * animation. This is reference to the listener for transition end added to
240 * the icon element while it's fading out.
241 * @type {?function(Event)}
242 * @private
244 hideTransitionListener_: null,
247 * Callback for click and 'Enter' key events that gets set if the icon is
248 * interactive.
249 * @type {?function()}
250 * @private
252 actionHandler_: null,
254 /** @override */
255 decorate: function() {
256 this.iconElement.addEventListener('mouseover',
257 this.showTooltipSoon_.bind(this));
258 this.iconElement.addEventListener('mouseout',
259 this.hideTooltip_.bind(this, false));
260 this.iconElement.addEventListener('mousedown',
261 this.handleMouseDown_.bind(this));
262 this.iconElement.addEventListener('click',
263 this.handleClick_.bind(this));
264 this.iconElement.addEventListener('keydown',
265 this.handleKeyDown_.bind(this));
267 // When the icon is focused using mouse, there should be no outline shown.
268 // Preventing default mousedown event accomplishes this.
269 this.iconElement.addEventListener('mousedown', function(e) {
270 e.preventDefault();
275 * Getter for the icon element's div.
276 * @return {HTMLDivElement}
278 get iconElement() {
279 return this.querySelector('.custom-icon');
283 * Updates the icon element class list to properly represent the provided
284 * icon.
285 * @param {!string} id The id of the icon that should be shown. Should be
286 * one of the ids listed in {@code UserPodCustomIcon.ICONS}.
288 setIcon: function(id) {
289 UserPodCustomIcon.ICONS.forEach(function(icon) {
290 this.iconElement.classList.toggle(icon.class, id == icon.id);
291 }, this);
295 * Sets the ARIA label for the icon.
296 * @param {!string} ariaLabel
298 setAriaLabel: function(ariaLabel) {
299 this.iconElement.setAttribute('aria-label', ariaLabel);
303 * Shows the icon.
305 show: function() {
306 this.resetHideTransitionState_();
307 this.hidden = false;
311 * Hides the icon using a fade-out animation.
313 fadeOut: function() {
314 // The icon is already being hidden.
315 if (this.iconElement.classList.contains('faded'))
316 return;
318 this.hideTooltip_(true);
319 this.iconElement.classList.add('faded');
320 this.hideTransitionListener_ = this.hide.bind(this);
321 this.iconElement.addEventListener('webkitTransitionEnd',
322 this.hideTransitionListener_);
323 ensureTransitionEndEvent(this.iconElement, 200);
327 * Updates the icon tooltip. If {@code autoshow} parameter is set the
328 * tooltip is immediatelly shown. If tooltip text is not set, the method
329 * ensures the tooltip gets hidden. If tooltip is shown prior to this call,
330 * it remains shown, but the tooltip text is updated.
331 * @param {!{text: string, autoshow: boolean}} tooltip The tooltip
332 * parameters.
334 setTooltip: function(tooltip) {
335 this.iconElement.classList.toggle('icon-with-tooltip', !!tooltip);
337 if (this.tooltip_ == tooltip.text && !tooltip.autoshow)
338 return;
339 this.tooltip_ = tooltip.text;
341 // If tooltip is already shown, just update the text.
342 if (tooltip.text && this.tooltipActive_ && !$('bubble').hidden) {
343 // If both previous and the new tooltip are supposed to be shown
344 // automatically, keep the autoshown flag.
345 var markAutoshown = this.tooltipAutoshown_ && tooltip.autoshow;
346 this.hideTooltip_(true);
347 this.showTooltip_();
348 this.tooltipAutoshown_ = markAutoshown;
349 return;
352 // If autoshow flag is set, make sure the tooltip gets shown.
353 if (tooltip.text && tooltip.autoshow) {
354 this.hideTooltip_(true);
355 this.showTooltip_();
356 this.tooltipAutoshown_ = true;
357 // Reset the tooltip active flag, which gets set in |showTooltip_|.
358 this.tooltipActive_ = false;
359 return;
362 this.hideTooltip_(true);
366 * Sets up icon tabIndex attribute and handler for click and 'Enter' key
367 * down events.
368 * @param {?function()} callback If icon should be interactive, the
369 * function to get called on click and 'Enter' key down events. Should
370 * be null to make the icon non interactive.
372 setInteractive: function(callback) {
373 this.iconElement.classList.toggle('interactive-custom-icon', !!callback);
375 // Update tabIndex property if needed.
376 if (!!this.actionHandler_ != !!callback) {
377 if (callback) {
378 this.iconElement.setAttribute('tabIndex',
379 UserPodTabOrder.POD_CUSTOM_ICON);
380 } else {
381 this.iconElement.removeAttribute('tabIndex');
385 // Set the new action handler.
386 this.actionHandler_ = callback;
390 * Hides the icon and cleans its state.
392 hide: function() {
393 this.hideTooltip_(true);
394 this.hidden = true;
395 this.setInteractive(null);
396 this.resetHideTransitionState_();
400 * Ensures the icon's transition state potentially set by a call to
401 * {@code fadeOut} is cleared.
402 * @private
404 resetHideTransitionState_: function() {
405 if (this.hideTransitionListener_) {
406 this.iconElement.removeEventListener('webkitTransitionEnd',
407 this.hideTransitionListener_);
408 this.hideTransitionListener_ = null;
410 this.iconElement.classList.toggle('faded', false);
414 * Handles mouse down event in the icon element.
415 * @param {Event} e The mouse down event.
416 * @private
418 handleMouseDown_: function(e) {
419 this.hideTooltip_(false);
420 // Stop the event propagation so in the case the click ends up on the
421 // user pod (outside the custom icon) auth is not attempted.
422 stopEventPropagation(e);
426 * Handles click event on the icon element. No-op if
427 * {@code this.actionHandler_} is not set.
428 * @param {Event} e The click event.
429 * @private
431 handleClick_: function(e) {
432 if (!this.actionHandler_)
433 return;
434 this.actionHandler_();
435 stopEventPropagation(e);
439 * Handles key down event on the icon element. Only 'Enter' key is handled.
440 * No-op if {@code this.actionHandler_} is not set.
441 * @param {Event} e The key down event.
442 * @private
444 handleKeyDown_: function(e) {
445 if (!this.actionHandler_ || e.keyIdentifier != 'Enter')
446 return;
447 this.actionHandler_(e);
448 stopEventPropagation(e);
452 * Called when mouse enters the icon. It sets timeout for showing the
453 * tooltip.
454 * @private
456 showTooltipSoon_: function() {
457 if (this.showTooltipTimeout_ || this.tooltipActive_)
458 return;
459 this.showTooltipTimeout_ =
460 setTimeout(this.showTooltip_.bind(this), 1000);
464 * Shows the current tooltip if one is set.
465 * @private
467 showTooltip_: function() {
468 if (this.hidden || !this.tooltip_ || this.tooltipActive_)
469 return;
471 // If autoshown bubble got hidden, clear the autoshown flag.
472 if ($('bubble').hidden && this.tooltipAutoshown_)
473 this.tooltipAutoshown_ = false;
475 // Show the tooltip bubble.
476 var bubbleContent = document.createElement('div');
477 bubbleContent.textContent = this.tooltip_;
479 /** @const */ var BUBBLE_OFFSET = CUSTOM_ICON_CONTAINER_SIZE / 2;
480 /** @const */ var BUBBLE_PADDING = 8;
481 $('bubble').showContentForElement(this,
482 cr.ui.Bubble.Attachment.RIGHT,
483 bubbleContent,
484 BUBBLE_OFFSET,
485 BUBBLE_PADDING);
486 this.ensureTooltipTimeoutCleared_();
487 this.tooltipActive_ = true;
491 * Hides the tooltip. If the current tooltip was automatically shown, it
492 * will be hidden only if |force| is set.
493 * @param {boolean} Whether the tooltip should be hidden if it got shown
494 * because autoshow flag was set.
495 * @private
497 hideTooltip_: function(force) {
498 this.tooltipActive_ = false;
499 this.ensureTooltipTimeoutCleared_();
500 if (!force && this.tooltipAutoshown_)
501 return;
502 $('bubble').hideForElement(this);
503 this.tooltipAutoshown_ = false;
507 * Clears timaout for showing the tooltip if it's set.
508 * @private
510 ensureTooltipTimeoutCleared_: function() {
511 if (this.showTooltipTimeout_) {
512 clearTimeout(this.showTooltipTimeout_);
513 this.showTooltipTimeout_ = null;
519 * Unique salt added to user image URLs to prevent caching. Dictionary with
520 * user names as keys.
521 * @type {Object}
523 UserPod.userImageSalt_ = {};
525 UserPod.prototype = {
526 __proto__: HTMLDivElement.prototype,
529 * Whether click on the pod can issue a user click auth attempt. The
530 * attempt can be issued iff the pod was focused when the click
531 * started (i.e. on mouse down event).
532 * @type {boolean}
533 * @private
535 userClickAuthAllowed_: false,
537 /** @override */
538 decorate: function() {
539 this.tabIndex = UserPodTabOrder.POD_INPUT;
540 this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX;
542 this.addEventListener('keydown', this.handlePodKeyDown_.bind(this));
543 this.addEventListener('click', this.handleClickOnPod_.bind(this));
544 this.addEventListener('mousedown', this.handlePodMouseDown_.bind(this));
546 this.signinButtonElement.addEventListener('click',
547 this.activate.bind(this));
549 this.actionBoxAreaElement.addEventListener('mousedown',
550 stopEventPropagation);
551 this.actionBoxAreaElement.addEventListener('click',
552 this.handleActionAreaButtonClick_.bind(this));
553 this.actionBoxAreaElement.addEventListener('keydown',
554 this.handleActionAreaButtonKeyDown_.bind(this));
556 this.actionBoxMenuRemoveElement.addEventListener('click',
557 this.handleRemoveCommandClick_.bind(this));
558 this.actionBoxMenuRemoveElement.addEventListener('keydown',
559 this.handleRemoveCommandKeyDown_.bind(this));
560 this.actionBoxMenuRemoveElement.addEventListener('blur',
561 this.handleRemoveCommandBlur_.bind(this));
562 this.actionBoxRemoveUserWarningButtonElement.addEventListener(
563 'click',
564 this.handleRemoveUserConfirmationClick_.bind(this));
565 this.actionBoxRemoveUserWarningButtonElement.addEventListener(
566 'keydown',
567 this.handleRemoveUserConfirmationKeyDown_.bind(this));
569 var customIcon = this.customIconElement;
570 customIcon.parentNode.replaceChild(new UserPodCustomIcon(), customIcon);
574 * Initializes the pod after its properties set and added to a pod row.
576 initialize: function() {
577 this.passwordElement.addEventListener('keydown',
578 this.parentNode.handleKeyDown.bind(this.parentNode));
579 this.passwordElement.addEventListener('keypress',
580 this.handlePasswordKeyPress_.bind(this));
582 this.imageElement.addEventListener('load',
583 this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
585 var initialAuthType = this.user.initialAuthType ||
586 AUTH_TYPE.OFFLINE_PASSWORD;
587 this.setAuthType(initialAuthType, null);
589 this.userClickAuthAllowed_ = false;
593 * Resets tab order for pod elements to its initial state.
595 resetTabOrder: function() {
596 // Note: the |mainInput| can be the pod itself.
597 this.mainInput.tabIndex = -1;
598 this.tabIndex = UserPodTabOrder.POD_INPUT;
602 * Handles keypress event (i.e. any textual input) on password input.
603 * @param {Event} e Keypress Event object.
604 * @private
606 handlePasswordKeyPress_: function(e) {
607 // When tabbing from the system tray a tab key press is received. Suppress
608 // this so as not to type a tab character into the password field.
609 if (e.keyCode == 9) {
610 e.preventDefault();
611 return;
616 * Top edge margin number of pixels.
617 * @type {?number}
619 set top(top) {
620 this.style.top = cr.ui.toCssPx(top);
624 * Top edge margin number of pixels.
626 get top() {
627 return parseInt(this.style.top);
631 * Left edge margin number of pixels.
632 * @type {?number}
634 set left(left) {
635 this.style.left = cr.ui.toCssPx(left);
639 * Left edge margin number of pixels.
641 get left() {
642 return parseInt(this.style.left);
646 * Height number of pixels.
648 get height() {
649 return this.offsetHeight;
653 * Gets image element.
654 * @type {!HTMLImageElement}
656 get imageElement() {
657 return this.querySelector('.user-image');
661 * Gets name element.
662 * @type {!HTMLDivElement}
664 get nameElement() {
665 return this.querySelector('.name');
669 * Gets the container holding the password field.
670 * @type {!HTMLInputElement}
672 get passwordEntryContainerElement() {
673 return this.querySelector('.password-entry-container');
677 * Gets password field.
678 * @type {!HTMLInputElement}
680 get passwordElement() {
681 return this.querySelector('.password');
685 * Gets the password label, which is used to show a message where the
686 * password field is normally.
687 * @type {!HTMLInputElement}
689 get passwordLabelElement() {
690 return this.querySelector('.password-label');
694 * Gets user sign in button.
695 * @type {!HTMLButtonElement}
697 get signinButtonElement() {
698 return this.querySelector('.signin-button');
702 * Gets the container holding the launch app button.
703 * @type {!HTMLButtonElement}
705 get launchAppButtonContainerElement() {
706 return this.querySelector('.launch-app-button-container');
710 * Gets launch app button.
711 * @type {!HTMLButtonElement}
713 get launchAppButtonElement() {
714 return this.querySelector('.launch-app-button');
718 * Gets action box area.
719 * @type {!HTMLInputElement}
721 get actionBoxAreaElement() {
722 return this.querySelector('.action-box-area');
726 * Gets user type icon area.
727 * @type {!HTMLDivElement}
729 get userTypeIconAreaElement() {
730 return this.querySelector('.user-type-icon-area');
734 * Gets user type bubble like multi-profiles policy restriction message.
735 * @type {!HTMLDivElement}
737 get userTypeBubbleElement() {
738 return this.querySelector('.user-type-bubble');
742 * Gets action box menu.
743 * @type {!HTMLInputElement}
745 get actionBoxMenu() {
746 return this.querySelector('.action-box-menu');
750 * Gets action box menu title, user name item.
751 * @type {!HTMLInputElement}
753 get actionBoxMenuTitleNameElement() {
754 return this.querySelector('.action-box-menu-title-name');
758 * Gets action box menu title, user email item.
759 * @type {!HTMLInputElement}
761 get actionBoxMenuTitleEmailElement() {
762 return this.querySelector('.action-box-menu-title-email');
766 * Gets action box menu, remove user command item.
767 * @type {!HTMLInputElement}
769 get actionBoxMenuCommandElement() {
770 return this.querySelector('.action-box-menu-remove-command');
774 * Gets action box menu, remove user command item div.
775 * @type {!HTMLInputElement}
777 get actionBoxMenuRemoveElement() {
778 return this.querySelector('.action-box-menu-remove');
782 * Gets action box menu, remove user warning text div.
783 * @type {!HTMLInputElement}
785 get actionBoxRemoveUserWarningTextElement() {
786 return this.querySelector('.action-box-remove-user-warning-text');
790 * Gets action box menu, remove supervised user warning text div.
791 * @type {!HTMLInputElement}
793 get actionBoxRemoveSupervisedUserWarningTextElement() {
794 return this.querySelector(
795 '.action-box-remove-supervised-user-warning-text');
799 * Gets action box menu, remove user command item div.
800 * @type {!HTMLInputElement}
802 get actionBoxRemoveUserWarningElement() {
803 return this.querySelector('.action-box-remove-user-warning');
807 * Gets action box menu, remove user command item div.
808 * @type {!HTMLInputElement}
810 get actionBoxRemoveUserWarningButtonElement() {
811 return this.querySelector('.remove-warning-button');
815 * Gets the custom icon. This icon is normally hidden, but can be shown
816 * using the chrome.screenlockPrivate API.
817 * @type {!HTMLDivElement}
819 get customIconElement() {
820 return this.querySelector('.custom-icon-container');
824 * Updates the user pod element.
826 update: function() {
827 this.imageElement.src = 'chrome://userimage/' + this.user.username +
828 '?id=' + UserPod.userImageSalt_[this.user.username];
830 this.nameElement.textContent = this.user_.displayName;
831 this.classList.toggle('signed-in', this.user_.signedIn);
833 if (this.isAuthTypeUserClick)
834 this.passwordLabelElement.textContent = this.authValue;
836 this.updateActionBoxArea();
838 this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
839 'passwordFieldAccessibleName', this.user_.emailAddress));
841 this.customizeUserPodPerUserType();
844 updateActionBoxArea: function() {
845 if (this.user_.publicAccount || this.user_.isApp) {
846 this.actionBoxAreaElement.hidden = true;
847 return;
850 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
852 this.actionBoxAreaElement.setAttribute(
853 'aria-label', loadTimeData.getStringF(
854 'podMenuButtonAccessibleName', this.user_.emailAddress));
855 this.actionBoxMenuRemoveElement.setAttribute(
856 'aria-label', loadTimeData.getString(
857 'podMenuRemoveItemAccessibleName'));
858 this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
859 loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
860 this.user_.displayName;
861 this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
862 this.actionBoxMenuTitleEmailElement.hidden = this.user_.supervisedUser;
864 this.actionBoxMenuCommandElement.textContent =
865 loadTimeData.getString('removeUser');
868 customizeUserPodPerUserType: function() {
869 if (this.user_.supervisedUser && !this.user_.isDesktopUser) {
870 this.setUserPodIconType('supervised');
871 } else if (this.multiProfilesPolicyApplied) {
872 // Mark user pod as not focusable which in addition to the grayed out
873 // filter makes it look in disabled state.
874 this.classList.add('multiprofiles-policy-applied');
875 this.setUserPodIconType('policy');
877 if (this.user.multiProfilesPolicy == 'primary-only')
878 this.querySelector('.mp-policy-primary-only-msg').hidden = false;
879 else if (this.user.multiProfilesPolicy == 'owner-primary-only')
880 this.querySelector('.mp-owner-primary-only-msg').hidden = false;
881 else
882 this.querySelector('.mp-policy-not-allowed-msg').hidden = false;
883 } else if (this.user_.isApp) {
884 this.setUserPodIconType('app');
888 setUserPodIconType: function(userTypeClass) {
889 this.userTypeIconAreaElement.classList.add(userTypeClass);
890 this.userTypeIconAreaElement.hidden = false;
894 * The user that this pod represents.
895 * @type {!Object}
897 user_: undefined,
898 get user() {
899 return this.user_;
901 set user(userDict) {
902 this.user_ = userDict;
903 this.update();
907 * Returns true if multi-profiles sign in is currently active and this
908 * user pod is restricted per policy.
909 * @type {boolean}
911 get multiProfilesPolicyApplied() {
912 var isMultiProfilesUI =
913 (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING);
914 return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed;
918 * Gets main input element.
919 * @type {(HTMLButtonElement|HTMLInputElement)}
921 get mainInput() {
922 if (this.isAuthTypePassword) {
923 return this.passwordElement;
924 } else if (this.isAuthTypeOnlineSignIn) {
925 return this.signinButtonElement;
926 } else if (this.isAuthTypeUserClick) {
927 return this.passwordLabelElement;
932 * Whether action box button is in active state.
933 * @type {boolean}
935 get isActionBoxMenuActive() {
936 return this.actionBoxAreaElement.classList.contains('active');
938 set isActionBoxMenuActive(active) {
939 if (active == this.isActionBoxMenuActive)
940 return;
942 if (active) {
943 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
944 this.actionBoxRemoveUserWarningElement.hidden = true;
946 // Clear focus first if another pod is focused.
947 if (!this.parentNode.isFocused(this)) {
948 this.parentNode.focusPod(undefined, true);
949 this.actionBoxAreaElement.focus();
952 // Hide user-type-bubble.
953 this.userTypeBubbleElement.classList.remove('bubble-shown');
955 this.actionBoxAreaElement.classList.add('active');
957 // If the user pod is on either edge of the screen, then the menu
958 // could be displayed partially ofscreen.
959 this.actionBoxMenu.classList.remove('left-edge-offset');
960 this.actionBoxMenu.classList.remove('right-edge-offset');
962 var offsetLeft =
963 cr.ui.login.DisplayManager.getOffset(this.actionBoxMenu).left;
964 var menuWidth = this.actionBoxMenu.offsetWidth;
965 if (offsetLeft < 0)
966 this.actionBoxMenu.classList.add('left-edge-offset');
967 else if (offsetLeft + menuWidth > window.innerWidth)
968 this.actionBoxMenu.classList.add('right-edge-offset');
969 } else {
970 this.actionBoxAreaElement.classList.remove('active');
971 this.actionBoxAreaElement.classList.remove('menu-moved-up');
972 this.actionBoxMenu.classList.remove('menu-moved-up');
977 * Whether action box button is in hovered state.
978 * @type {boolean}
980 get isActionBoxMenuHovered() {
981 return this.actionBoxAreaElement.classList.contains('hovered');
983 set isActionBoxMenuHovered(hovered) {
984 if (hovered == this.isActionBoxMenuHovered)
985 return;
987 if (hovered) {
988 this.actionBoxAreaElement.classList.add('hovered');
989 this.classList.add('hovered');
990 } else {
991 if (this.multiProfilesPolicyApplied)
992 this.userTypeBubbleElement.classList.remove('bubble-shown');
993 this.actionBoxAreaElement.classList.remove('hovered');
994 this.classList.remove('hovered');
999 * Set the authentication type for the pod.
1000 * @param {number} An auth type value defined in the AUTH_TYPE enum.
1001 * @param {string} authValue The initial value used for the auth type.
1003 setAuthType: function(authType, authValue) {
1004 this.authType_ = authType;
1005 this.authValue_ = authValue;
1006 this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]);
1007 this.update();
1008 this.reset(this.parentNode.isFocused(this));
1012 * The auth type of the user pod. This value is one of the enum
1013 * values in AUTH_TYPE.
1014 * @type {number}
1016 get authType() {
1017 return this.authType_;
1021 * The initial value used for the pod's authentication type.
1022 * eg. a prepopulated password input when using password authentication.
1024 get authValue() {
1025 return this.authValue_;
1029 * True if the the user pod uses a password to authenticate.
1030 * @type {bool}
1032 get isAuthTypePassword() {
1033 return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD ||
1034 this.authType_ == AUTH_TYPE.FORCE_OFFLINE_PASSWORD;
1038 * True if the the user pod uses a user click to authenticate.
1039 * @type {bool}
1041 get isAuthTypeUserClick() {
1042 return this.authType_ == AUTH_TYPE.USER_CLICK;
1046 * True if the the user pod uses a online sign in to authenticate.
1047 * @type {bool}
1049 get isAuthTypeOnlineSignIn() {
1050 return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN;
1054 * Updates the image element of the user.
1056 updateUserImage: function() {
1057 UserPod.userImageSalt_[this.user.username] = new Date().getTime();
1058 this.update();
1062 * Focuses on input element.
1064 focusInput: function() {
1065 // Move tabIndex from the whole pod to the main input.
1066 // Note: the |mainInput| can be the pod itself.
1067 this.tabIndex = -1;
1068 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1069 this.mainInput.focus();
1073 * Activates the pod.
1074 * @param {Event} e Event object.
1075 * @return {boolean} True if activated successfully.
1077 activate: function(e) {
1078 if (this.isAuthTypeOnlineSignIn) {
1079 this.showSigninUI();
1080 } else if (this.isAuthTypeUserClick) {
1081 Oobe.disableSigninUI();
1082 chrome.send('attemptUnlock', [this.user.username]);
1083 } else if (this.isAuthTypePassword) {
1084 if (!this.passwordElement.value)
1085 return false;
1086 Oobe.disableSigninUI();
1087 chrome.send('authenticateUser',
1088 [this.user.username, this.passwordElement.value]);
1089 } else {
1090 console.error('Activating user pod with invalid authentication type: ' +
1091 this.authType);
1094 return true;
1097 showSupervisedUserSigninWarning: function() {
1098 // Supervised user token has been invalidated.
1099 // Make sure that pod is focused i.e. "Sign in" button is seen.
1100 this.parentNode.focusPod(this);
1102 var error = document.createElement('div');
1103 var messageDiv = document.createElement('div');
1104 messageDiv.className = 'error-message-bubble';
1105 messageDiv.textContent =
1106 loadTimeData.getString('supervisedUserExpiredTokenWarning');
1107 error.appendChild(messageDiv);
1109 $('bubble').showContentForElement(
1110 this.signinButtonElement,
1111 cr.ui.Bubble.Attachment.TOP,
1112 error,
1113 this.signinButtonElement.offsetWidth / 2,
1115 // Move warning bubble up if it overlaps the shelf.
1116 var maxHeight =
1117 cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping($('bubble'));
1118 if (maxHeight < $('bubble').offsetHeight) {
1119 $('bubble').showContentForElement(
1120 this.signinButtonElement,
1121 cr.ui.Bubble.Attachment.BOTTOM,
1122 error,
1123 this.signinButtonElement.offsetWidth / 2,
1129 * Shows signin UI for this user.
1131 showSigninUI: function() {
1132 if (this.user.supervisedUser && !this.user.isDesktopUser) {
1133 this.showSupervisedUserSigninWarning();
1134 } else {
1135 // Special case for multi-profiles sign in. We show users even if they
1136 // are not allowed per policy. Restrict those users from starting GAIA.
1137 if (this.multiProfilesPolicyApplied)
1138 return;
1140 this.parentNode.showSigninUI(this.user.emailAddress);
1145 * Resets the input field and updates the tab order of pod controls.
1146 * @param {boolean} takeFocus If true, input field takes focus.
1148 reset: function(takeFocus) {
1149 this.passwordElement.value = '';
1150 if (takeFocus) {
1151 if (!this.multiProfilesPolicyApplied)
1152 this.focusInput(); // This will set a custom tab order.
1154 else
1155 this.resetTabOrder();
1159 * Removes a user using the correct identifier based on user type.
1160 * @param {Object} user User to be removed.
1162 removeUser: function(user) {
1163 chrome.send('removeUser',
1164 [user.isDesktopUser ? user.profilePath : user.username]);
1168 * Handles a click event on action area button.
1169 * @param {Event} e Click event.
1171 handleActionAreaButtonClick_: function(e) {
1172 if (this.parentNode.disabled)
1173 return;
1174 this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
1175 e.stopPropagation();
1179 * Handles a keydown event on action area button.
1180 * @param {Event} e KeyDown event.
1182 handleActionAreaButtonKeyDown_: function(e) {
1183 if (this.disabled)
1184 return;
1185 switch (e.keyIdentifier) {
1186 case 'Enter':
1187 case 'U+0020': // Space
1188 if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
1189 this.isActionBoxMenuActive = true;
1190 e.stopPropagation();
1191 break;
1192 case 'Up':
1193 case 'Down':
1194 if (this.isActionBoxMenuActive) {
1195 this.actionBoxMenuRemoveElement.tabIndex =
1196 UserPodTabOrder.PAD_MENU_ITEM;
1197 this.actionBoxMenuRemoveElement.focus();
1199 e.stopPropagation();
1200 break;
1201 case 'U+001B': // Esc
1202 this.isActionBoxMenuActive = false;
1203 e.stopPropagation();
1204 break;
1205 case 'U+0009': // Tab
1206 if (!this.parentNode.alwaysFocusSinglePod)
1207 this.parentNode.focusPod();
1208 default:
1209 this.isActionBoxMenuActive = false;
1210 break;
1215 * Handles a click event on remove user command.
1216 * @param {Event} e Click event.
1218 handleRemoveCommandClick_: function(e) {
1219 if (this.user.supervisedUser || this.user.isDesktopUser) {
1220 this.showRemoveWarning_();
1221 return;
1223 if (this.isActionBoxMenuActive)
1224 chrome.send('removeUser', [this.user.username]);
1228 * Shows remove user warning. Used for supervised users on CrOS, and for all
1229 * users on desktop.
1231 showRemoveWarning_: function() {
1232 this.actionBoxMenuRemoveElement.hidden = true;
1233 this.actionBoxRemoveUserWarningElement.hidden = false;
1234 this.actionBoxRemoveUserWarningButtonElement.focus();
1236 // Move up the menu if it overlaps shelf.
1237 var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(
1238 this.actionBoxMenu);
1239 var actualHeight = parseInt(
1240 window.getComputedStyle(this.actionBoxMenu).height);
1241 if (maxHeight < actualHeight) {
1242 this.actionBoxMenu.classList.add('menu-moved-up');
1243 this.actionBoxAreaElement.classList.add('menu-moved-up');
1248 * Handles a click event on remove user confirmation button.
1249 * @param {Event} e Click event.
1251 handleRemoveUserConfirmationClick_: function(e) {
1252 if (this.isActionBoxMenuActive) {
1253 this.isActionBoxMenuActive = false;
1254 this.removeUser(this.user);
1255 e.stopPropagation();
1260 * Handles a keydown event on remove user confirmation button.
1261 * @param {Event} e KeyDown event.
1263 handleRemoveUserConfirmationKeyDown_: function(e) {
1264 if (!this.isActionBoxMenuActive)
1265 return;
1267 // Only handle pressing 'Enter' or 'Space', and let all other events
1268 // bubble to the action box menu.
1269 if (e.keyIdentifier == 'Enter' || e.keyIdentifier == 'U+0020') {
1270 this.isActionBoxMenuActive = false;
1271 this.removeUser(this.user);
1272 e.stopPropagation();
1273 // Prevent default so that we don't trigger a 'click' event.
1274 e.preventDefault();
1279 * Handles a keydown event on remove command.
1280 * @param {Event} e KeyDown event.
1282 handleRemoveCommandKeyDown_: function(e) {
1283 if (this.disabled)
1284 return;
1285 switch (e.keyIdentifier) {
1286 case 'Enter':
1287 if (this.user.supervisedUser || this.user.isDesktopUser) {
1288 // Prevent default so that we don't trigger a 'click' event on the
1289 // remove button that will be focused.
1290 e.preventDefault();
1291 this.showRemoveWarning_();
1292 } else {
1293 this.removeUser(this.user);
1295 e.stopPropagation();
1296 break;
1297 case 'Up':
1298 case 'Down':
1299 e.stopPropagation();
1300 break;
1301 case 'U+001B': // Esc
1302 this.actionBoxAreaElement.focus();
1303 this.isActionBoxMenuActive = false;
1304 e.stopPropagation();
1305 break;
1306 default:
1307 this.actionBoxAreaElement.focus();
1308 this.isActionBoxMenuActive = false;
1309 break;
1314 * Handles a blur event on remove command.
1315 * @param {Event} e Blur event.
1317 handleRemoveCommandBlur_: function(e) {
1318 if (this.disabled)
1319 return;
1320 this.actionBoxMenuRemoveElement.tabIndex = -1;
1324 * Handles mouse down event. It sets whether the user click auth will be
1325 * allowed on the next mouse click event. The auth is allowed iff the pod
1326 * was focused on the mouse down event starting the click.
1327 * @param {Event} e The mouse down event.
1329 handlePodMouseDown_: function(e) {
1330 this.userClickAuthAllowed_ = this.parentNode.isFocused(this);
1334 * Handles click event on a user pod.
1335 * @param {Event} e Click event.
1337 handleClickOnPod_: function(e) {
1338 if (this.parentNode.disabled)
1339 return;
1341 if (!this.isActionBoxMenuActive) {
1342 if (this.isAuthTypeOnlineSignIn) {
1343 this.showSigninUI();
1344 } else if (this.isAuthTypeUserClick && this.userClickAuthAllowed_) {
1345 // Note that this.userClickAuthAllowed_ is set in mouse down event
1346 // handler.
1347 this.parentNode.setActivatedPod(this);
1350 if (this.multiProfilesPolicyApplied)
1351 this.userTypeBubbleElement.classList.add('bubble-shown');
1353 // Prevent default so that we don't trigger 'focus' event.
1354 e.preventDefault();
1359 * Handles keydown event for a user pod.
1360 * @param {Event} e Key event.
1362 handlePodKeyDown_: function(e) {
1363 if (!this.isAuthTypeUserClick || this.disabled)
1364 return;
1365 switch (e.keyIdentifier) {
1366 case 'Enter':
1367 case 'U+0020': // Space
1368 if (this.parentNode.isFocused(this))
1369 this.parentNode.setActivatedPod(this);
1370 break;
1376 * Creates a public account user pod.
1377 * @constructor
1378 * @extends {UserPod}
1380 var PublicAccountUserPod = cr.ui.define(function() {
1381 var node = UserPod();
1383 var extras = $('public-account-user-pod-extras-template').children;
1384 for (var i = 0; i < extras.length; ++i) {
1385 var el = extras[i].cloneNode(true);
1386 node.appendChild(el);
1389 return node;
1392 PublicAccountUserPod.prototype = {
1393 __proto__: UserPod.prototype,
1396 * "Enter" button in expanded side pane.
1397 * @type {!HTMLButtonElement}
1399 get enterButtonElement() {
1400 return this.querySelector('.enter-button');
1404 * Boolean flag of whether the pod is showing the side pane. The flag
1405 * controls whether 'expanded' class is added to the pod's class list and
1406 * resets tab order because main input element changes when the 'expanded'
1407 * state changes.
1408 * @type {boolean}
1410 get expanded() {
1411 return this.classList.contains('expanded');
1414 set expanded(expanded) {
1415 if (this.expanded == expanded)
1416 return;
1418 this.resetTabOrder();
1419 this.classList.toggle('expanded', expanded);
1420 if (expanded) {
1421 // Show the advanced expanded pod directly if there are at least two
1422 // recommended locales. This will be the case in multilingual
1423 // environments where users are likely to want to choose among locales.
1424 if (this.querySelector('.language-select').multipleRecommendedLocales)
1425 this.classList.add('advanced');
1426 this.usualLeft = this.left;
1427 this.makeSpaceForExpandedPod_();
1428 } else if (typeof(this.usualLeft) != 'undefined') {
1429 this.left = this.usualLeft;
1432 var self = this;
1433 this.classList.add('animating');
1434 this.addEventListener('webkitTransitionEnd', function f(e) {
1435 self.removeEventListener('webkitTransitionEnd', f);
1436 self.classList.remove('animating');
1438 // Accessibility focus indicator does not move with the focused
1439 // element. Sends a 'focus' event on the currently focused element
1440 // so that accessibility focus indicator updates its location.
1441 if (document.activeElement)
1442 document.activeElement.dispatchEvent(new Event('focus'));
1444 // Guard timer set to animation duration + 20ms.
1445 ensureTransitionEndEvent(this, 200);
1448 get advanced() {
1449 return this.classList.contains('advanced');
1452 /** @override */
1453 get mainInput() {
1454 if (this.expanded)
1455 return this.enterButtonElement;
1456 else
1457 return this.nameElement;
1460 /** @override */
1461 decorate: function() {
1462 UserPod.prototype.decorate.call(this);
1464 this.classList.add('public-account');
1466 this.nameElement.addEventListener('keydown', (function(e) {
1467 if (e.keyIdentifier == 'Enter') {
1468 this.parentNode.setActivatedPod(this, e);
1469 // Stop this keydown event from bubbling up to PodRow handler.
1470 e.stopPropagation();
1471 // Prevent default so that we don't trigger a 'click' event on the
1472 // newly focused "Enter" button.
1473 e.preventDefault();
1475 }).bind(this));
1477 var learnMore = this.querySelector('.learn-more');
1478 learnMore.addEventListener('mousedown', stopEventPropagation);
1479 learnMore.addEventListener('click', this.handleLearnMoreEvent);
1480 learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1482 learnMore = this.querySelector('.expanded-pane-learn-more');
1483 learnMore.addEventListener('click', this.handleLearnMoreEvent);
1484 learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
1486 var languageSelect = this.querySelector('.language-select');
1487 languageSelect.tabIndex = UserPodTabOrder.POD_INPUT;
1488 languageSelect.manuallyChanged = false;
1489 languageSelect.addEventListener(
1490 'change',
1491 function() {
1492 languageSelect.manuallyChanged = true;
1493 this.getPublicSessionKeyboardLayouts_();
1494 }.bind(this));
1496 var keyboardSelect = this.querySelector('.keyboard-select');
1497 keyboardSelect.tabIndex = UserPodTabOrder.POD_INPUT;
1498 keyboardSelect.loadedLocale = null;
1500 var languageAndInput = this.querySelector('.language-and-input');
1501 languageAndInput.tabIndex = UserPodTabOrder.POD_INPUT;
1502 languageAndInput.addEventListener('click',
1503 this.transitionToAdvanced_.bind(this));
1505 this.enterButtonElement.addEventListener('click', (function(e) {
1506 this.enterButtonElement.disabled = true;
1507 var locale = this.querySelector('.language-select').value;
1508 var keyboardSelect = this.querySelector('.keyboard-select');
1509 // The contents of |keyboardSelect| is updated asynchronously. If its
1510 // locale does not match |locale|, it has not updated yet and the
1511 // currently selected keyboard layout may not be applicable to |locale|.
1512 // Do not return any keyboard layout in this case and let the backend
1513 // choose a suitable layout.
1514 var keyboardLayout =
1515 keyboardSelect.loadedLocale == locale ? keyboardSelect.value : '';
1516 chrome.send('launchPublicSession',
1517 [this.user.username, locale, keyboardLayout]);
1518 }).bind(this));
1521 /** @override **/
1522 initialize: function() {
1523 UserPod.prototype.initialize.call(this);
1525 id = this.user.username + '-keyboard';
1526 this.querySelector('.keyboard-select-label').htmlFor = id;
1527 this.querySelector('.keyboard-select').setAttribute('id', id);
1529 var id = this.user.username + '-language';
1530 this.querySelector('.language-select-label').htmlFor = id;
1531 var languageSelect = this.querySelector('.language-select');
1532 languageSelect.setAttribute('id', id);
1533 this.populateLanguageSelect(this.user.initialLocales,
1534 this.user.initialLocale,
1535 this.user.initialMultipleRecommendedLocales);
1538 /** @override **/
1539 update: function() {
1540 UserPod.prototype.update.call(this);
1541 this.querySelector('.expanded-pane-name').textContent =
1542 this.user_.displayName;
1543 this.querySelector('.info').textContent =
1544 loadTimeData.getStringF('publicAccountInfoFormat',
1545 this.user_.enterpriseDomain);
1548 /** @override */
1549 focusInput: function() {
1550 // Move tabIndex from the whole pod to the main input.
1551 this.tabIndex = -1;
1552 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1553 this.mainInput.focus();
1556 /** @override */
1557 reset: function(takeFocus) {
1558 if (!takeFocus)
1559 this.expanded = false;
1560 this.enterButtonElement.disabled = false;
1561 UserPod.prototype.reset.call(this, takeFocus);
1564 /** @override */
1565 activate: function(e) {
1566 if (!this.expanded) {
1567 this.expanded = true;
1568 this.focusInput();
1570 return true;
1573 /** @override */
1574 handleClickOnPod_: function(e) {
1575 if (this.parentNode.disabled)
1576 return;
1578 this.parentNode.focusPod(this);
1579 this.parentNode.setActivatedPod(this, e);
1580 // Prevent default so that we don't trigger 'focus' event.
1581 e.preventDefault();
1585 * Updates the display name shown on the pod.
1586 * @param {string} displayName The new display name
1588 setDisplayName: function(displayName) {
1589 this.user_.displayName = displayName;
1590 this.update();
1594 * Handle mouse and keyboard events for the learn more button. Triggering
1595 * the button causes information about public sessions to be shown.
1596 * @param {Event} event Mouse or keyboard event.
1598 handleLearnMoreEvent: function(event) {
1599 switch (event.type) {
1600 // Show informaton on left click. Let any other clicks propagate.
1601 case 'click':
1602 if (event.button != 0)
1603 return;
1604 break;
1605 // Show informaton when <Return> or <Space> is pressed. Let any other
1606 // key presses propagate.
1607 case 'keydown':
1608 switch (event.keyCode) {
1609 case 13: // Return.
1610 case 32: // Space.
1611 break;
1612 default:
1613 return;
1615 break;
1617 chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
1618 stopEventPropagation(event);
1621 makeSpaceForExpandedPod_: function() {
1622 var width = this.classList.contains('advanced') ?
1623 PUBLIC_EXPANDED_ADVANCED_WIDTH : PUBLIC_EXPANDED_BASIC_WIDTH;
1624 var isDesktopUserManager = Oobe.getInstance().displayType ==
1625 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1626 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
1627 POD_ROW_PADDING;
1628 if (this.left + width > $('pod-row').offsetWidth - rowPadding)
1629 this.left = $('pod-row').offsetWidth - rowPadding - width;
1633 * Transition the expanded pod from the basic to the advanced view.
1635 transitionToAdvanced_: function() {
1636 var pod = this;
1637 var languageAndInputSection =
1638 this.querySelector('.language-and-input-section');
1639 this.classList.add('transitioning-to-advanced');
1640 setTimeout(function() {
1641 pod.classList.add('advanced');
1642 pod.makeSpaceForExpandedPod_();
1643 languageAndInputSection.addEventListener('webkitTransitionEnd',
1644 function observer() {
1645 languageAndInputSection.removeEventListener('webkitTransitionEnd',
1646 observer);
1647 pod.classList.remove('transitioning-to-advanced');
1648 pod.querySelector('.language-select').focus();
1650 // Guard timer set to animation duration + 20ms.
1651 ensureTransitionEndEvent(languageAndInputSection, 380);
1652 }, 0);
1656 * Retrieves the list of keyboard layouts available for the currently
1657 * selected locale.
1659 getPublicSessionKeyboardLayouts_: function() {
1660 var selectedLocale = this.querySelector('.language-select').value;
1661 if (selectedLocale ==
1662 this.querySelector('.keyboard-select').loadedLocale) {
1663 // If the list of keyboard layouts was loaded for the currently selected
1664 // locale, it is already up to date.
1665 return;
1667 chrome.send('getPublicSessionKeyboardLayouts',
1668 [this.user.username, selectedLocale]);
1672 * Populates the keyboard layout "select" element with a list of layouts.
1673 * @param {string} locale The locale to which this list of keyboard layouts
1674 * applies
1675 * @param {!Object} list List of available keyboard layouts
1677 populateKeyboardSelect: function(locale, list) {
1678 if (locale != this.querySelector('.language-select').value) {
1679 // The selected locale has changed and the list of keyboard layouts is
1680 // not applicable. This method will be called again when a list of
1681 // keyboard layouts applicable to the selected locale is retrieved.
1682 return;
1685 var keyboardSelect = this.querySelector('.keyboard-select');
1686 keyboardSelect.loadedLocale = locale;
1687 keyboardSelect.innerHTML = '';
1688 for (var i = 0; i < list.length; ++i) {
1689 var item = list[i];
1690 keyboardSelect.appendChild(
1691 new Option(item.title, item.value, item.selected, item.selected));
1696 * Populates the language "select" element with a list of locales.
1697 * @param {!Object} locales The list of available locales
1698 * @param {string} defaultLocale The locale to select by default
1699 * @param {boolean} multipleRecommendedLocales Whether |locales| contains
1700 * two or more recommended locales
1702 populateLanguageSelect: function(locales,
1703 defaultLocale,
1704 multipleRecommendedLocales) {
1705 var languageSelect = this.querySelector('.language-select');
1706 // If the user manually selected a locale, do not change the selection.
1707 // Otherwise, select the new |defaultLocale|.
1708 var selected =
1709 languageSelect.manuallyChanged ? languageSelect.value : defaultLocale;
1710 languageSelect.innerHTML = '';
1711 var group = languageSelect;
1712 for (var i = 0; i < locales.length; ++i) {
1713 var item = locales[i];
1714 if (item.optionGroupName) {
1715 group = document.createElement('optgroup');
1716 group.label = item.optionGroupName;
1717 languageSelect.appendChild(group);
1718 } else {
1719 group.appendChild(new Option(item.title,
1720 item.value,
1721 item.value == selected,
1722 item.value == selected));
1725 languageSelect.multipleRecommendedLocales = multipleRecommendedLocales;
1727 // Retrieve a list of keyboard layouts applicable to the locale that is
1728 // now selected.
1729 this.getPublicSessionKeyboardLayouts_();
1734 * Creates a user pod to be used only in desktop chrome.
1735 * @constructor
1736 * @extends {UserPod}
1738 var DesktopUserPod = cr.ui.define(function() {
1739 // Don't just instantiate a UserPod(), as this will call decorate() on the
1740 // parent object, and add duplicate event listeners.
1741 var node = $('user-pod-template').cloneNode(true);
1742 node.removeAttribute('id');
1743 return node;
1746 DesktopUserPod.prototype = {
1747 __proto__: UserPod.prototype,
1749 /** @override */
1750 get mainInput() {
1751 if (this.user.needsSignin)
1752 return this.passwordElement;
1753 else
1754 return this.nameElement;
1757 /** @override */
1758 update: function() {
1759 this.imageElement.src = this.user.userImage;
1760 this.nameElement.textContent = this.user.displayName;
1762 var isLockedUser = this.user.needsSignin;
1763 var isSupervisedUser = this.user.supervisedUser;
1764 this.classList.toggle('locked', isLockedUser);
1765 this.classList.toggle('supervised-user', isSupervisedUser);
1767 if (this.isAuthTypeUserClick)
1768 this.passwordLabelElement.textContent = this.authValue;
1770 this.actionBoxRemoveUserWarningTextElement.hidden = isSupervisedUser;
1771 this.actionBoxRemoveSupervisedUserWarningTextElement.hidden =
1772 !isSupervisedUser;
1774 UserPod.prototype.updateActionBoxArea.call(this);
1777 /** @override */
1778 focusInput: function() {
1779 // Move tabIndex from the whole pod to the main input.
1780 this.tabIndex = -1;
1781 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1782 this.mainInput.focus();
1785 /** @override */
1786 activate: function(e) {
1787 if (!this.user.needsSignin) {
1788 Oobe.launchUser(this.user.emailAddress, this.user.displayName);
1789 } else if (!this.passwordElement.value) {
1790 return false;
1791 } else {
1792 chrome.send('authenticatedLaunchUser',
1793 [this.user.emailAddress,
1794 this.user.displayName,
1795 this.passwordElement.value]);
1797 this.passwordElement.value = '';
1798 return true;
1801 /** @override */
1802 handleClickOnPod_: function(e) {
1803 if (this.parentNode.disabled)
1804 return;
1806 Oobe.clearErrors();
1807 this.parentNode.lastFocusedPod_ = this;
1809 // If this is an unlocked pod, then open a browser window. Otherwise
1810 // just activate the pod and show the password field.
1811 if (!this.user.needsSignin && !this.isActionBoxMenuActive)
1812 this.activate(e);
1814 if (this.isAuthTypeUserClick)
1815 chrome.send('attemptUnlock', [this.user.emailAddress]);
1820 * Creates a user pod that represents kiosk app.
1821 * @constructor
1822 * @extends {UserPod}
1824 var KioskAppPod = cr.ui.define(function() {
1825 var node = UserPod();
1826 return node;
1829 KioskAppPod.prototype = {
1830 __proto__: UserPod.prototype,
1832 /** @override */
1833 decorate: function() {
1834 UserPod.prototype.decorate.call(this);
1835 this.launchAppButtonElement.addEventListener('click',
1836 this.activate.bind(this));
1839 /** @override */
1840 update: function() {
1841 this.imageElement.src = this.user.iconUrl;
1842 this.imageElement.alt = this.user.label;
1843 this.imageElement.title = this.user.label;
1844 this.passwordEntryContainerElement.hidden = true;
1845 this.launchAppButtonContainerElement.hidden = false;
1846 this.nameElement.textContent = this.user.label;
1848 UserPod.prototype.updateActionBoxArea.call(this);
1849 UserPod.prototype.customizeUserPodPerUserType.call(this);
1852 /** @override */
1853 get mainInput() {
1854 return this.launchAppButtonElement;
1857 /** @override */
1858 focusInput: function() {
1859 // Move tabIndex from the whole pod to the main input.
1860 this.tabIndex = -1;
1861 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1862 this.mainInput.focus();
1865 /** @override */
1866 get forceOnlineSignin() {
1867 return false;
1870 /** @override */
1871 activate: function(e) {
1872 var diagnosticMode = e && e.ctrlKey;
1873 this.launchApp_(this.user, diagnosticMode);
1874 return true;
1877 /** @override */
1878 handleClickOnPod_: function(e) {
1879 if (this.parentNode.disabled)
1880 return;
1882 Oobe.clearErrors();
1883 this.parentNode.lastFocusedPod_ = this;
1884 this.activate(e);
1888 * Launch the app. If |diagnosticMode| is true, ask user to confirm.
1889 * @param {Object} app App data.
1890 * @param {boolean} diagnosticMode Whether to run the app in diagnostic
1891 * mode.
1893 launchApp_: function(app, diagnosticMode) {
1894 if (!diagnosticMode) {
1895 chrome.send('launchKioskApp', [app.id, false]);
1896 return;
1899 var oobe = $('oobe');
1900 if (!oobe.confirmDiagnosticMode_) {
1901 oobe.confirmDiagnosticMode_ =
1902 new cr.ui.dialogs.ConfirmDialog(document.body);
1903 oobe.confirmDiagnosticMode_.setOkLabel(
1904 loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
1905 oobe.confirmDiagnosticMode_.setCancelLabel(
1906 loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
1909 oobe.confirmDiagnosticMode_.show(
1910 loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
1911 app.label),
1912 function() {
1913 chrome.send('launchKioskApp', [app.id, true]);
1919 * Creates a new pod row element.
1920 * @constructor
1921 * @extends {HTMLDivElement}
1923 var PodRow = cr.ui.define('podrow');
1925 PodRow.prototype = {
1926 __proto__: HTMLDivElement.prototype,
1928 // Whether this user pod row is shown for the first time.
1929 firstShown_: true,
1931 // True if inside focusPod().
1932 insideFocusPod_: false,
1934 // Focused pod.
1935 focusedPod_: undefined,
1937 // Activated pod, i.e. the pod of current login attempt.
1938 activatedPod_: undefined,
1940 // Pod that was most recently focused, if any.
1941 lastFocusedPod_: undefined,
1943 // Pods whose initial images haven't been loaded yet.
1944 podsWithPendingImages_: [],
1946 // Whether pod placement has been postponed.
1947 podPlacementPostponed_: false,
1949 // Standard user pod height/width.
1950 userPodHeight_: 0,
1951 userPodWidth_: 0,
1953 // Array of apps that are shown in addition to other user pods.
1954 apps_: [],
1956 // True to show app pods along with user pods.
1957 shouldShowApps_: true,
1959 // Array of users that are shown (public/supervised/regular).
1960 users_: [],
1962 // If we're disabling single pod autofocus for Touch View.
1963 touchViewSinglePodExperimentOn_: true,
1966 /** @override */
1967 decorate: function() {
1968 // Event listeners that are installed for the time period during which
1969 // the element is visible.
1970 this.listeners_ = {
1971 focus: [this.handleFocus_.bind(this), true /* useCapture */],
1972 click: [this.handleClick_.bind(this), true],
1973 mousemove: [this.handleMouseMove_.bind(this), false],
1974 keydown: [this.handleKeyDown.bind(this), false]
1977 var isDesktopUserManager = Oobe.getInstance().displayType ==
1978 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
1979 this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT :
1980 CROS_POD_HEIGHT;
1981 // Same for Chrome OS and desktop.
1982 this.userPodWidth_ = POD_WIDTH;
1986 * Returns all the pods in this pod row.
1987 * @type {NodeList}
1989 get pods() {
1990 return Array.prototype.slice.call(this.children);
1994 * Return true if user pod row has only single user pod in it, which should
1995 * always be focused except desktop and touch view modes.
1996 * @type {boolean}
1998 get alwaysFocusSinglePod() {
1999 var isDesktopUserManager = Oobe.getInstance().displayType ==
2000 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2002 return (isDesktopUserManager ||
2003 (this.touchViewSinglePodExperimentOn_ &&
2004 this.touchViewEnabled_)) ?
2005 false : this.children.length == 1;
2009 * Returns pod with the given app id.
2010 * @param {!string} app_id Application id to be matched.
2011 * @return {Object} Pod with the given app id. null if pod hasn't been
2012 * found.
2014 getPodWithAppId_: function(app_id) {
2015 for (var i = 0, pod; pod = this.pods[i]; ++i) {
2016 if (pod.user.isApp && pod.user.id == app_id)
2017 return pod;
2019 return null;
2023 * Returns pod with the given username (null if there is no such pod).
2024 * @param {string} username Username to be matched.
2025 * @return {Object} Pod with the given username. null if pod hasn't been
2026 * found.
2028 getPodWithUsername_: function(username) {
2029 for (var i = 0, pod; pod = this.pods[i]; ++i) {
2030 if (pod.user.username == username)
2031 return pod;
2033 return null;
2037 * True if the the pod row is disabled (handles no user interaction).
2038 * @type {boolean}
2040 disabled_: false,
2041 get disabled() {
2042 return this.disabled_;
2044 set disabled(value) {
2045 this.disabled_ = value;
2046 var controls = this.querySelectorAll('button,input');
2047 for (var i = 0, control; control = controls[i]; ++i) {
2048 control.disabled = value;
2053 * Creates a user pod from given email.
2054 * @param {!Object} user User info dictionary.
2056 createUserPod: function(user) {
2057 var userPod;
2058 if (user.isDesktopUser)
2059 userPod = new DesktopUserPod({user: user});
2060 else if (user.publicAccount)
2061 userPod = new PublicAccountUserPod({user: user});
2062 else if (user.isApp)
2063 userPod = new KioskAppPod({user: user});
2064 else
2065 userPod = new UserPod({user: user});
2067 userPod.hidden = false;
2068 return userPod;
2072 * Add an existing user pod to this pod row.
2073 * @param {!Object} user User info dictionary.
2075 addUserPod: function(user) {
2076 var userPod = this.createUserPod(user);
2077 this.appendChild(userPod);
2078 userPod.initialize();
2082 * Runs app with a given id from the list of loaded apps.
2083 * @param {!string} app_id of an app to run.
2084 * @param {boolean=} opt_diagnostic_mode Whether to run the app in
2085 * diagnostic mode. Default is false.
2087 findAndRunAppForTesting: function(app_id, opt_diagnostic_mode) {
2088 var app = this.getPodWithAppId_(app_id);
2089 if (app) {
2090 var activationEvent = cr.doc.createEvent('MouseEvents');
2091 var ctrlKey = opt_diagnostic_mode;
2092 activationEvent.initMouseEvent('click', true, true, null,
2093 0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
2094 app.dispatchEvent(activationEvent);
2099 * Removes user pod from pod row.
2100 * @param {string} email User's email.
2102 removeUserPod: function(username) {
2103 var podToRemove = this.getPodWithUsername_(username);
2104 if (podToRemove == null) {
2105 console.warn('Attempt to remove not existing pod for ' + username +
2106 '.');
2107 return;
2109 this.removeChild(podToRemove);
2110 if (this.pods.length > 0)
2111 this.placePods_();
2115 * Returns index of given pod or -1 if not found.
2116 * @param {UserPod} pod Pod to look up.
2117 * @private
2119 indexOf_: function(pod) {
2120 for (var i = 0; i < this.pods.length; ++i) {
2121 if (pod == this.pods[i])
2122 return i;
2124 return -1;
2128 * Populates pod row with given existing users and start init animation.
2129 * @param {array} users Array of existing user emails.
2131 loadPods: function(users) {
2132 this.users_ = users;
2134 this.rebuildPods();
2138 * Scrolls focused user pod into view.
2140 scrollFocusedPodIntoView: function() {
2141 var pod = this.focusedPod_;
2142 if (!pod)
2143 return;
2145 // First check whether focused pod is already fully visible.
2146 var visibleArea = $('scroll-container');
2147 var scrollTop = visibleArea.scrollTop;
2148 var clientHeight = visibleArea.clientHeight;
2149 var podTop = $('oobe').offsetTop + pod.offsetTop;
2150 var padding = USER_POD_KEYBOARD_MIN_PADDING;
2151 if (podTop + pod.height + padding <= scrollTop + clientHeight &&
2152 podTop - padding >= scrollTop) {
2153 return;
2156 // Scroll so that user pod is as centered as possible.
2157 visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2;
2161 * Rebuilds pod row using users_ and apps_ that were previously set or
2162 * updated.
2164 rebuildPods: function() {
2165 var emptyPodRow = this.pods.length == 0;
2167 // Clear existing pods.
2168 this.innerHTML = '';
2169 this.focusedPod_ = undefined;
2170 this.activatedPod_ = undefined;
2171 this.lastFocusedPod_ = undefined;
2173 // Switch off animation
2174 Oobe.getInstance().toggleClass('flying-pods', false);
2176 // Populate the pod row.
2177 for (var i = 0; i < this.users_.length; ++i)
2178 this.addUserPod(this.users_[i]);
2180 for (var i = 0, pod; pod = this.pods[i]; ++i)
2181 this.podsWithPendingImages_.push(pod);
2183 // TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
2184 if (this.shouldShowApps_) {
2185 for (var i = 0; i < this.apps_.length; ++i)
2186 this.addUserPod(this.apps_[i]);
2189 // Make sure we eventually show the pod row, even if some image is stuck.
2190 setTimeout(function() {
2191 $('pod-row').classList.remove('images-loading');
2192 }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
2194 var isAccountPicker = $('login-header-bar').signinUIState ==
2195 SIGNIN_UI_STATE.ACCOUNT_PICKER;
2197 // Immediately recalculate pods layout only when current UI is account
2198 // picker. Otherwise postpone it.
2199 if (isAccountPicker) {
2200 this.placePods_();
2201 this.maybePreselectPod();
2203 // Without timeout changes in pods positions will be animated even
2204 // though it happened when 'flying-pods' class was disabled.
2205 setTimeout(function() {
2206 Oobe.getInstance().toggleClass('flying-pods', true);
2207 }, 0);
2208 } else {
2209 this.podPlacementPostponed_ = true;
2211 // Update [Cancel] button state.
2212 if ($('login-header-bar').signinUIState ==
2213 SIGNIN_UI_STATE.GAIA_SIGNIN &&
2214 emptyPodRow &&
2215 this.pods.length > 0) {
2216 login.GaiaSigninScreen.updateCancelButtonState();
2222 * Adds given apps to the pod row.
2223 * @param {array} apps Array of apps.
2225 setApps: function(apps) {
2226 this.apps_ = apps;
2227 this.rebuildPods();
2228 chrome.send('kioskAppsLoaded');
2230 // Check whether there's a pending kiosk app error.
2231 window.setTimeout(function() {
2232 chrome.send('checkKioskAppLaunchError');
2233 }, 500);
2237 * Sets whether should show app pods.
2238 * @param {boolean} shouldShowApps Whether app pods should be shown.
2240 setShouldShowApps: function(shouldShowApps) {
2241 if (this.shouldShowApps_ == shouldShowApps)
2242 return;
2244 this.shouldShowApps_ = shouldShowApps;
2245 this.rebuildPods();
2249 * Shows a custom icon on a user pod besides the input field.
2250 * @param {string} username Username of pod to add button
2251 * @param {!{id: !string,
2252 * hardlockOnClick: boolean,
2253 * ariaLabel: string | undefined,
2254 * tooltip: ({text: string, autoshow: boolean} | undefined)}} icon
2255 * The icon parameters.
2257 showUserPodCustomIcon: function(username, icon) {
2258 var pod = this.getPodWithUsername_(username);
2259 if (pod == null) {
2260 console.error('Unable to show user pod button for ' + username +
2261 ': user pod not found.');
2262 return;
2265 if (!icon.id)
2266 return;
2268 pod.customIconElement.setIcon(icon.id);
2270 if (icon.hardlockOnClick) {
2271 pod.customIconElement.setInteractive(
2272 this.hardlockUserPod_.bind(this, username));
2273 } else {
2274 pod.customIconElement.setInteractive(null);
2277 var ariaLabel = icon.ariaLabel || (icon.tooltip && icon.tooltip.text);
2278 if (ariaLabel)
2279 pod.customIconElement.setAriaLabel(ariaLabel);
2280 else
2281 console.warn('No ARIA label for user pod custom icon.');
2283 pod.customIconElement.show();
2285 // This has to be called after |show| in case the tooltip should be shown
2286 // immediatelly.
2287 pod.customIconElement.setTooltip(
2288 icon.tooltip || {text: '', autoshow: false});
2292 * Hard-locks user pod for the user. If user pod is hard-locked, it can be
2293 * only unlocked using password, and the authentication type cannot be
2294 * changed.
2295 * @param {!string} username The user's username.
2296 * @private
2298 hardlockUserPod_: function(username) {
2299 chrome.send('hardlockPod', [username]);
2303 * Hides the custom icon in the user pod added by showUserPodCustomIcon().
2304 * @param {string} username Username of pod to remove button
2306 hideUserPodCustomIcon: function(username) {
2307 var pod = this.getPodWithUsername_(username);
2308 if (pod == null) {
2309 console.error('Unable to hide user pod button for ' + username +
2310 ': user pod not found.');
2311 return;
2314 // TODO(tengs): Allow option for a fading transition.
2315 pod.customIconElement.hide();
2319 * Sets the authentication type used to authenticate the user.
2320 * @param {string} username Username of selected user
2321 * @param {number} authType Authentication type, must be one of the
2322 * values listed in AUTH_TYPE enum.
2323 * @param {string} value The initial value to use for authentication.
2325 setAuthType: function(username, authType, value) {
2326 var pod = this.getPodWithUsername_(username);
2327 if (pod == null) {
2328 console.error('Unable to set auth type for ' + username +
2329 ': user pod not found.');
2330 return;
2332 pod.setAuthType(authType, value);
2336 * Sets the state of touch view mode.
2337 * @param {boolean} isTouchViewEnabled true if the mode is on.
2339 setTouchViewState: function(isTouchViewEnabled) {
2340 this.touchViewEnabled_ = isTouchViewEnabled;
2344 * Updates the display name shown on a public session pod.
2345 * @param {string} userID The user ID of the public session
2346 * @param {string} displayName The new display name
2348 setPublicSessionDisplayName: function(userID, displayName) {
2349 var pod = this.getPodWithUsername_(userID);
2350 if (pod != null)
2351 pod.setDisplayName(displayName);
2355 * Updates the list of locales available for a public session.
2356 * @param {string} userID The user ID of the public session
2357 * @param {!Object} locales The list of available locales
2358 * @param {string} defaultLocale The locale to select by default
2359 * @param {boolean} multipleRecommendedLocales Whether |locales| contains
2360 * two or more recommended locales
2362 setPublicSessionLocales: function(userID,
2363 locales,
2364 defaultLocale,
2365 multipleRecommendedLocales) {
2366 var pod = this.getPodWithUsername_(userID);
2367 if (pod != null) {
2368 pod.populateLanguageSelect(locales,
2369 defaultLocale,
2370 multipleRecommendedLocales);
2375 * Updates the list of available keyboard layouts for a public session pod.
2376 * @param {string} userID The user ID of the public session
2377 * @param {string} locale The locale to which this list of keyboard layouts
2378 * applies
2379 * @param {!Object} list List of available keyboard layouts
2381 setPublicSessionKeyboardLayouts: function(userID, locale, list) {
2382 var pod = this.getPodWithUsername_(userID);
2383 if (pod != null)
2384 pod.populateKeyboardSelect(locale, list);
2388 * Called when window was resized.
2390 onWindowResize: function() {
2391 var layout = this.calculateLayout_();
2392 if (layout.columns != this.columns || layout.rows != this.rows)
2393 this.placePods_();
2395 if (Oobe.getInstance().virtualKeyboardShown)
2396 this.scrollFocusedPodIntoView();
2400 * Returns width of podrow having |columns| number of columns.
2401 * @private
2403 columnsToWidth_: function(columns) {
2404 var isDesktopUserManager = Oobe.getInstance().displayType ==
2405 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2406 var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2407 MARGIN_BY_COLUMNS[columns];
2408 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2409 POD_ROW_PADDING;
2410 return 2 * rowPadding + columns * this.userPodWidth_ +
2411 (columns - 1) * margin;
2415 * Returns height of podrow having |rows| number of rows.
2416 * @private
2418 rowsToHeight_: function(rows) {
2419 var isDesktopUserManager = Oobe.getInstance().displayType ==
2420 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2421 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2422 POD_ROW_PADDING;
2423 return 2 * rowPadding + rows * this.userPodHeight_;
2427 * Calculates number of columns and rows that podrow should have in order to
2428 * hold as much its pods as possible for current screen size. Also it tries
2429 * to choose layout that looks good.
2430 * @return {{columns: number, rows: number}}
2432 calculateLayout_: function() {
2433 var preferredColumns = this.pods.length < COLUMNS.length ?
2434 COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
2435 var maxWidth = Oobe.getInstance().clientAreaSize.width;
2436 var columns = preferredColumns;
2437 while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
2438 --columns;
2439 var rows = Math.floor((this.pods.length - 1) / columns) + 1;
2440 if (getComputedStyle(
2441 $('signin-banner'), null).getPropertyValue('display') != 'none') {
2442 rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER);
2444 var maxHeigth = Oobe.getInstance().clientAreaSize.height;
2445 while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
2446 --rows;
2447 // One more iteration if it's not enough cells to place all pods.
2448 while (maxWidth >= this.columnsToWidth_(columns + 1) &&
2449 columns * rows < this.pods.length &&
2450 columns < MAX_NUMBER_OF_COLUMNS) {
2451 ++columns;
2453 return {columns: columns, rows: rows};
2457 * Places pods onto their positions onto pod grid.
2458 * @private
2460 placePods_: function() {
2461 var layout = this.calculateLayout_();
2462 var columns = this.columns = layout.columns;
2463 var rows = this.rows = layout.rows;
2464 var maxPodsNumber = columns * rows;
2465 var isDesktopUserManager = Oobe.getInstance().displayType ==
2466 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2467 var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
2468 MARGIN_BY_COLUMNS[columns];
2469 this.parentNode.setPreferredSize(
2470 this.columnsToWidth_(columns), this.rowsToHeight_(rows));
2471 var height = this.userPodHeight_;
2472 var width = this.userPodWidth_;
2473 this.pods.forEach(function(pod, index) {
2474 if (index >= maxPodsNumber) {
2475 pod.hidden = true;
2476 return;
2478 pod.hidden = false;
2479 if (pod.offsetHeight != height) {
2480 console.error('Pod offsetHeight (' + pod.offsetHeight +
2481 ') and POD_HEIGHT (' + height + ') are not equal.');
2483 if (pod.offsetWidth != width) {
2484 console.error('Pod offsetWidth (' + pod.offsetWidth +
2485 ') and POD_WIDTH (' + width + ') are not equal.');
2487 var column = index % columns;
2488 var row = Math.floor(index / columns);
2489 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2490 POD_ROW_PADDING;
2491 pod.left = rowPadding + column * (width + margin);
2493 // On desktop, we want the rows to always be equally spaced.
2494 pod.top = isDesktopUserManager ? row * (height + rowPadding) :
2495 row * height + rowPadding;
2497 Oobe.getInstance().updateScreenSize(this.parentNode);
2501 * Number of columns.
2502 * @type {?number}
2504 set columns(columns) {
2505 // Cannot use 'columns' here.
2506 this.setAttribute('ncolumns', columns);
2508 get columns() {
2509 return parseInt(this.getAttribute('ncolumns'));
2513 * Number of rows.
2514 * @type {?number}
2516 set rows(rows) {
2517 // Cannot use 'rows' here.
2518 this.setAttribute('nrows', rows);
2520 get rows() {
2521 return parseInt(this.getAttribute('nrows'));
2525 * Whether the pod is currently focused.
2526 * @param {UserPod} pod Pod to check for focus.
2527 * @return {boolean} Pod focus status.
2529 isFocused: function(pod) {
2530 return this.focusedPod_ == pod;
2534 * Focuses a given user pod or clear focus when given null.
2535 * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
2536 * @param {boolean=} opt_force If true, forces focus update even when
2537 * podToFocus is already focused.
2539 focusPod: function(podToFocus, opt_force) {
2540 if (this.isFocused(podToFocus) && !opt_force) {
2541 // Calling focusPod w/o podToFocus means reset.
2542 if (!podToFocus)
2543 Oobe.clearErrors();
2544 this.keyboardActivated_ = false;
2545 return;
2548 // Make sure there's only one focusPod operation happening at a time.
2549 if (this.insideFocusPod_) {
2550 this.keyboardActivated_ = false;
2551 return;
2553 this.insideFocusPod_ = true;
2555 for (var i = 0, pod; pod = this.pods[i]; ++i) {
2556 if (!this.alwaysFocusSinglePod) {
2557 pod.isActionBoxMenuActive = false;
2559 if (pod != podToFocus) {
2560 pod.isActionBoxMenuHovered = false;
2561 pod.classList.remove('focused');
2562 // On Desktop, the faded style is not set correctly, so we should
2563 // manually fade out non-focused pods if there is a focused pod.
2564 if (pod.user.isDesktopUser && podToFocus)
2565 pod.classList.add('faded');
2566 else
2567 pod.classList.remove('faded');
2568 pod.reset(false);
2572 // Clear any error messages for previous pod.
2573 if (!this.isFocused(podToFocus))
2574 Oobe.clearErrors();
2576 var hadFocus = !!this.focusedPod_;
2577 this.focusedPod_ = podToFocus;
2578 if (podToFocus) {
2579 podToFocus.classList.remove('faded');
2580 podToFocus.classList.add('focused');
2581 if (!podToFocus.multiProfilesPolicyApplied)
2582 podToFocus.reset(true); // Reset and give focus.
2583 else {
2584 podToFocus.userTypeBubbleElement.classList.add('bubble-shown');
2585 podToFocus.focus();
2588 // focusPod() automatically loads wallpaper
2589 if (!podToFocus.user.isApp)
2590 chrome.send('focusPod', [podToFocus.user.username]);
2591 this.firstShown_ = false;
2592 this.lastFocusedPod_ = podToFocus;
2594 if (Oobe.getInstance().virtualKeyboardShown)
2595 this.scrollFocusedPodIntoView();
2597 this.insideFocusPod_ = false;
2598 this.keyboardActivated_ = false;
2602 * Resets wallpaper to the last active user's wallpaper, if any.
2604 loadLastWallpaper: function() {
2605 if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp)
2606 chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
2610 * Returns the currently activated pod.
2611 * @type {UserPod}
2613 get activatedPod() {
2614 return this.activatedPod_;
2618 * Sets currently activated pod.
2619 * @param {UserPod} pod Pod to check for focus.
2620 * @param {Event} e Event object.
2622 setActivatedPod: function(pod, e) {
2623 if (pod && pod.activate(e))
2624 this.activatedPod_ = pod;
2628 * The pod of the signed-in user, if any; null otherwise.
2629 * @type {?UserPod}
2631 get lockedPod() {
2632 for (var i = 0, pod; pod = this.pods[i]; ++i) {
2633 if (pod.user.signedIn)
2634 return pod;
2636 return null;
2640 * The pod that is preselected on user pod row show.
2641 * @type {?UserPod}
2643 get preselectedPod() {
2644 var isDesktopUserManager = Oobe.getInstance().displayType ==
2645 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2646 if (isDesktopUserManager) {
2647 // On desktop, don't pre-select a pod if it's the only one.
2648 if (this.pods.length == 1)
2649 return null;
2651 // The desktop User Manager can send the index of a pod that should be
2652 // initially focused in url hash.
2653 var podIndex = parseInt(window.location.hash.substr(1));
2654 if (isNaN(podIndex) || podIndex >= this.pods.length)
2655 return null;
2656 return this.pods[podIndex];
2659 var lockedPod = this.lockedPod;
2660 if (lockedPod)
2661 return lockedPod;
2662 for (var i = 0, pod; pod = this.pods[i]; ++i) {
2663 if (!pod.multiProfilesPolicyApplied) {
2664 return pod;
2667 return this.pods[0];
2671 * Resets input UI.
2672 * @param {boolean} takeFocus True to take focus.
2674 reset: function(takeFocus) {
2675 this.disabled = false;
2676 if (this.activatedPod_)
2677 this.activatedPod_.reset(takeFocus);
2681 * Restores input focus to current selected pod, if there is any.
2683 refocusCurrentPod: function() {
2684 if (this.focusedPod_ && !this.focusedPod_.multiProfilesPolicyApplied) {
2685 this.focusedPod_.focusInput();
2690 * Clears focused pod password field.
2692 clearFocusedPod: function() {
2693 if (!this.disabled && this.focusedPod_)
2694 this.focusedPod_.reset(true);
2698 * Shows signin UI.
2699 * @param {string} email Email for signin UI.
2701 showSigninUI: function(email) {
2702 // Clear any error messages that might still be around.
2703 Oobe.clearErrors();
2704 this.disabled = true;
2705 this.lastFocusedPod_ = this.getPodWithUsername_(email);
2706 Oobe.showSigninUI(email);
2710 * Updates current image of a user.
2711 * @param {string} username User for which to update the image.
2713 updateUserImage: function(username) {
2714 var pod = this.getPodWithUsername_(username);
2715 if (pod)
2716 pod.updateUserImage();
2720 * Handler of click event.
2721 * @param {Event} e Click Event object.
2722 * @private
2724 handleClick_: function(e) {
2725 if (this.disabled)
2726 return;
2728 // Clear all menus if the click is outside pod menu and its
2729 // button area.
2730 if (!findAncestorByClass(e.target, 'action-box-menu') &&
2731 !findAncestorByClass(e.target, 'action-box-area')) {
2732 for (var i = 0, pod; pod = this.pods[i]; ++i)
2733 pod.isActionBoxMenuActive = false;
2736 // Clears focus if not clicked on a pod and if there's more than one pod.
2737 var pod = findAncestorByClass(e.target, 'pod');
2738 if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) {
2739 this.focusPod();
2742 if (pod)
2743 pod.isActionBoxMenuHovered = true;
2745 // Return focus back to single pod.
2746 if (this.alwaysFocusSinglePod && !pod) {
2747 this.focusPod(this.focusedPod_, true /* force */);
2748 this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
2749 this.focusedPod_.isActionBoxMenuHovered = false;
2754 * Handler of mouse move event.
2755 * @param {Event} e Click Event object.
2756 * @private
2758 handleMouseMove_: function(e) {
2759 if (this.disabled)
2760 return;
2761 if (e.webkitMovementX == 0 && e.webkitMovementY == 0)
2762 return;
2764 // Defocus (thus hide) action box, if it is focused on a user pod
2765 // and the pointer is not hovering over it.
2766 var pod = findAncestorByClass(e.target, 'pod');
2767 if (document.activeElement &&
2768 document.activeElement.parentNode != pod &&
2769 document.activeElement.classList.contains('action-box-area')) {
2770 document.activeElement.parentNode.focus();
2773 if (pod)
2774 pod.isActionBoxMenuHovered = true;
2776 // Hide action boxes on other user pods.
2777 for (var i = 0, p; p = this.pods[i]; ++i)
2778 if (p != pod && !p.isActionBoxMenuActive)
2779 p.isActionBoxMenuHovered = false;
2783 * Handles focus event.
2784 * @param {Event} e Focus Event object.
2785 * @private
2787 handleFocus_: function(e) {
2788 if (this.disabled)
2789 return;
2790 if (e.target.parentNode == this) {
2791 // Focus on a pod
2792 if (e.target.classList.contains('focused')) {
2793 if (!e.target.multiProfilesPolicyApplied)
2794 e.target.focusInput();
2795 else
2796 e.target.userTypeBubbleElement.classList.add('bubble-shown');
2797 } else
2798 this.focusPod(e.target);
2799 return;
2802 var pod = findAncestorByClass(e.target, 'pod');
2803 if (pod && pod.parentNode == this) {
2804 // Focus on a control of a pod but not on the action area button.
2805 if (!pod.classList.contains('focused') &&
2806 !e.target.classList.contains('action-box-button')) {
2807 this.focusPod(pod);
2808 pod.userTypeBubbleElement.classList.remove('bubble-shown');
2809 e.target.focus();
2811 return;
2814 // Clears pod focus when we reach here. It means new focus is neither
2815 // on a pod nor on a button/input for a pod.
2816 // Do not "defocus" user pod when it is a single pod.
2817 // That means that 'focused' class will not be removed and
2818 // input field/button will always be visible.
2819 if (!this.alwaysFocusSinglePod)
2820 this.focusPod();
2821 else {
2822 // Hide user-type-bubble in case this is one pod and we lost focus of
2823 // it.
2824 this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
2829 * Handler of keydown event.
2830 * @param {Event} e KeyDown Event object.
2832 handleKeyDown: function(e) {
2833 if (this.disabled)
2834 return;
2835 var editing = e.target.tagName == 'INPUT' && e.target.value;
2836 switch (e.keyIdentifier) {
2837 case 'Left':
2838 if (!editing) {
2839 this.keyboardActivated_ = true;
2840 if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
2841 this.focusPod(this.focusedPod_.previousElementSibling);
2842 else
2843 this.focusPod(this.lastElementChild);
2845 e.stopPropagation();
2847 break;
2848 case 'Right':
2849 if (!editing) {
2850 this.keyboardActivated_ = true;
2851 if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
2852 this.focusPod(this.focusedPod_.nextElementSibling);
2853 else
2854 this.focusPod(this.firstElementChild);
2856 e.stopPropagation();
2858 break;
2859 case 'Enter':
2860 if (this.focusedPod_) {
2861 var targetTag = e.target.tagName;
2862 if (e.target == this.focusedPod_.passwordElement ||
2863 (targetTag != 'INPUT' &&
2864 targetTag != 'BUTTON' &&
2865 targetTag != 'A')) {
2866 this.setActivatedPod(this.focusedPod_, e);
2867 e.stopPropagation();
2870 break;
2871 case 'U+001B': // Esc
2872 if (!this.alwaysFocusSinglePod)
2873 this.focusPod();
2874 break;
2879 * Called right after the pod row is shown.
2881 handleAfterShow: function() {
2882 // Without timeout changes in pods positions will be animated even though
2883 // it happened when 'flying-pods' class was disabled.
2884 setTimeout(function() {
2885 Oobe.getInstance().toggleClass('flying-pods', true);
2886 }, 0);
2887 // Force input focus for user pod on show and once transition ends.
2888 if (this.focusedPod_) {
2889 var focusedPod = this.focusedPod_;
2890 var screen = this.parentNode;
2891 var self = this;
2892 focusedPod.addEventListener('webkitTransitionEnd', function f(e) {
2893 focusedPod.removeEventListener('webkitTransitionEnd', f);
2894 focusedPod.reset(true);
2895 // Notify screen that it is ready.
2896 screen.onShow();
2898 // Guard timer for 1 second -- it would conver all possible animations.
2899 ensureTransitionEndEvent(focusedPod, 1000);
2904 * Called right before the pod row is shown.
2906 handleBeforeShow: function() {
2907 Oobe.getInstance().toggleClass('flying-pods', false);
2908 for (var event in this.listeners_) {
2909 this.ownerDocument.addEventListener(
2910 event, this.listeners_[event][0], this.listeners_[event][1]);
2912 $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
2914 if (this.podPlacementPostponed_) {
2915 this.podPlacementPostponed_ = false;
2916 this.placePods_();
2917 this.maybePreselectPod();
2922 * Called when the element is hidden.
2924 handleHide: function() {
2925 for (var event in this.listeners_) {
2926 this.ownerDocument.removeEventListener(
2927 event, this.listeners_[event][0], this.listeners_[event][1]);
2929 $('login-header-bar').buttonsTabIndex = 0;
2933 * Called when a pod's user image finishes loading.
2935 handlePodImageLoad: function(pod) {
2936 var index = this.podsWithPendingImages_.indexOf(pod);
2937 if (index == -1) {
2938 return;
2941 this.podsWithPendingImages_.splice(index, 1);
2942 if (this.podsWithPendingImages_.length == 0) {
2943 this.classList.remove('images-loading');
2948 * Preselects pod, if needed.
2950 maybePreselectPod: function() {
2951 var pod = this.preselectedPod;
2952 this.focusPod(pod);
2954 // Hide user-type-bubble in case all user pods are disabled and we focus
2955 // first pod.
2956 if (pod && pod.multiProfilesPolicyApplied) {
2957 pod.userTypeBubbleElement.classList.remove('bubble-shown');
2962 return {
2963 PodRow: PodRow