blob: 6f3d565895ed84122ad6efe872450458ea6d5bd8 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview User pod row implementation.
*/
cr.define('login', function() {
/**
* Variables used for pod placement processing. Width and height should be
* synced with computed CSS sizes of pods.
*/
var CROS_POD_WIDTH = 306;
var CROS_SMALL_POD_WIDTH = 304;
var CROS_EXTRA_SMALL_POD_WIDTH = 282;
var DESKTOP_POD_WIDTH = 180;
var MD_DESKTOP_POD_WIDTH = 160;
var PUBLIC_EXPANDED_WIDTH = 622;
var CROS_POD_HEIGHT = 346;
var CROS_SMALL_POD_HEIGHT = 74;
var CROS_EXTRA_SMALL_POD_HEIGHT = 60;
var DESKTOP_POD_HEIGHT = 226;
var MD_DESKTOP_POD_HEIGHT = 200;
var PUBLIC_EXPANDED_HEIGHT = 324;
var POD_ROW_PADDING = 10;
var DESKTOP_ROW_PADDING = 32;
var CUSTOM_ICON_CONTAINER_SIZE = 40;
var CROS_PIN_POD_HEIGHT = 417;
var SCROLL_MASK_HEIGHT = 112;
var CROS_POD_HEIGHT_WITH_PIN = 618;
var PUBLIC_SESSION_ICON_WIDTH = 12;
/**
* The maximum number of users that each pod placement method can handle.
*/
var POD_ROW_LIMIT = 2;
var LANDSCAPE_MODE_LIMIT = 6;
var PORTRAIT_MODE_LIMIT = 9;
/**
* Distance between the bubble and user pod.
* @type {number}
* @const
*/
var BUBBLE_POD_OFFSET = 4;
/**
* Maximum time for which the pod row remains hidden until all user images
* have been loaded.
* @type {number}
* @const
*/
var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
/**
* The duration of the animation for switching between main pod and small
* pod. It should be synced with CSS.
* @type {number}
* @const
*/
var POD_SWITCH_ANIMATION_DURATION_MS = 180;
/**
* Public session help topic identifier.
* @type {number}
* @const
*/
var HELP_TOPIC_PUBLIC_SESSION = 3041033;
/**
* Tab order for user pods. Update these when adding new controls.
* @enum {number}
* @const
*/
var UserPodTabOrder = {
POD_INPUT: 1, // Password input field, action box menu button and
// the pod itself.
PIN_KEYBOARD: 2, // Pin keyboard below the password input field.
POD_CUSTOM_ICON: 3, // Pod custom icon next to password input field.
HEADER_BAR: 4, // Buttons on the header bar (Shutdown, Add User).
POD_MENU_ITEM: 5 // User pod menu items (User info, Remove user).
};
/**
* Supported authentication types. Keep in sync with the enum in
* components/proximity_auth/public/interfaces/auth_type.mojom
* @enum {number}
* @const
*/
var AUTH_TYPE = {
OFFLINE_PASSWORD: 0,
ONLINE_SIGN_IN: 1,
NUMERIC_PIN: 2,
USER_CLICK: 3,
EXPAND_THEN_USER_CLICK: 4,
FORCE_OFFLINE_PASSWORD: 5
};
/**
* Names of authentication types.
*/
var AUTH_TYPE_NAMES = {
0: 'offlinePassword',
1: 'onlineSignIn',
2: 'numericPin',
3: 'userClick',
4: 'expandThenUserClick',
5: 'forceOfflinePassword'
};
/**
* Supported fingerprint unlock states.
* @enum {number}
* @const
*/
var FINGERPRINT_STATES = {
HIDDEN: 0,
DEFAULT: 1,
SIGNIN: 2,
FAILED: 3,
};
/**
* The fingerprint states to classes mapping.
* {@code state} properties indicate current fingerprint unlock state.
* {@code class} properties are CSS classes used to set the icons' background
* and password placeholder color.
* @const {Array<{type: !number, class: !string}>}
*/
var FINGERPRINT_STATES_MAPPING = [
{state: FINGERPRINT_STATES.HIDDEN, class: 'hidden'},
{state: FINGERPRINT_STATES.DEFAULT, class: 'default'},
{state: FINGERPRINT_STATES.SIGNIN, class: 'signin'},
{state: FINGERPRINT_STATES.FAILED, class: 'failed'}
];
// Supported multi-profile user behavior values.
// Keep in sync with the enum in login_user_info.mojom
var MULTI_PROFILE_USER_BEHAVIOR = {
UNRESTRICTED: 0,
PRIMARY_ONLY: 1,
NOT_ALLOWED: 2,
OWNER_PRIMARY_ONLY: 3
};
// Focus and tab order are organized as follows:
//
// (1) all user pods have tab index 1 so they are traversed first;
// (2) when a user pod is activated, its tab index is set to -1, then its
// main input field and action box menu button get focus;
// (3) if pin keyboard is present, it has tab index 2 so it follows the
// action box menu button;
// (4) if user pod custom icon is interactive, it has tab index 3;
// (5) buttons on the header bar have tab index 4;
// (6) User pod menu items (if present) have tab index 5;
// (7) lastly, focus jumps to the Status Area and back to user pods.
//
// 'Focus' event is handled by a capture handler for the whole document
// and in some cases 'mousedown' event handlers are used instead of 'click'
// handlers where it's necessary to prevent 'focus' event from being fired.
/**
* Helper function to remove a class from given element.
* @param {!HTMLElement} el Element whose class list to change.
* @param {string} cl Class to remove.
*/
function removeClass(el, cl) {
el.classList.remove(cl);
}
/**
* Helper function to switch directions for right-to-left languages.
* @param {!HTMLElement} el Element whose style needs to change.
*/
function switchDirection(el) {
var leftStyle = el.style.left;
el.style.left = el.style.right;
el.style.right = leftStyle;
}
/**
* Creates a user pod.
* @constructor
* @extends {HTMLDivElement}
*/
var UserPod = cr.ui.define(function() {
var node = $('user-pod-template').cloneNode(true);
node.removeAttribute('id');
return node;
});
/**
* The display style of user pods.
* @enum {number}
* @const
*/
UserPod.Style = {
LARGE: 0,
SMALL: 1,
EXTRA_SMALL: 2
};
/**
* Stops event propagation from the any user pod child element.
* @param {Event} e Event to handle.
*/
function stopEventPropagation(e) {
// Prevent default so that we don't trigger a 'focus' event.
e.preventDefault();
e.stopPropagation();
}
/**
* Creates an element for custom icon shown in a user pod next to the input
* field.
* @constructor
* @extends {HTMLDivElement}
*/
var UserPodCustomIcon = cr.ui.define(function() {
var node = document.createElement('div');
node.classList.add('custom-icon-container');
node.hidden = true;
// Create the actual icon element and add it as a child to the container.
var iconNode = document.createElement('div');
iconNode.classList.add('custom-icon');
node.appendChild(iconNode);
return node;
});
/**
* The supported user pod custom icons.
* {@code id} properties should be in sync with values set by C++ side.
* {@code class} properties are CSS classes used to set the icons' background.
* @const {Array<{id: !string, class: !string}>}
*/
UserPodCustomIcon.ICONS = [
{id: 'locked', class: 'custom-icon-locked'},
{id: 'locked-to-be-activated',
class: 'custom-icon-locked-to-be-activated'},
{id: 'locked-with-proximity-hint',
class: 'custom-icon-locked-with-proximity-hint'},
{id: 'unlocked', class: 'custom-icon-unlocked'},
{id: 'hardlocked', class: 'custom-icon-hardlocked'},
{id: 'spinner', class: 'custom-icon-spinner'}
];
/**
* The hover state for the icon. When user hovers over the icon, a tooltip
* should be shown after a short delay. This enum is used to keep track of
* the tooltip status related to hover state.
* @enum {string}
*/
UserPodCustomIcon.HoverState = {
/** The user is not hovering over the icon. */
NO_HOVER: 'no_hover',
/** The user is hovering over the icon but the tooltip is not activated. */
HOVER: 'hover',
/**
* User is hovering over the icon and the tooltip is activated due to the
* hover state (which happens with delay after user starts hovering).
*/
HOVER_TOOLTIP: 'hover_tooltip'
};
/**
* If the icon has a tooltip that should be automatically shown, the tooltip
* is shown even when there is no user action (i.e. user is not hovering over
* the icon), after a short delay. The tooltip should be hidden after some
* time. Note that the icon will not be considered autoshown if it was
* previously shown as a result of the user action.
* This enum is used to keep track of this state.
* @enum {string}
*/
UserPodCustomIcon.TooltipAutoshowState = {
/** The tooltip should not be or was not automatically shown. */
DISABLED: 'disabled',
/**
* The tooltip should be automatically shown, but the timeout for showing
* the tooltip has not yet passed.
*/
ENABLED: 'enabled',
/** The tooltip was automatically shown. */
ACTIVE : 'active'
};
UserPodCustomIcon.prototype = {
__proto__: HTMLDivElement.prototype,
/**
* The id of the icon being shown.
* @type {string}
* @private
*/
iconId_: '',
/**
* A reference to the timeout for updating icon hover state. Non-null
* only if there is an active timeout.
* @type {?number}
* @private
*/
updateHoverStateTimeout_: null,
/**
* A reference to the timeout for updating icon tooltip autoshow state.
* Non-null only if there is an active timeout.
* @type {?number}
* @private
*/
updateTooltipAutoshowStateTimeout_: null,
/**
* Callback for click and 'Enter' key events that gets set if the icon is
* interactive.
* @type {?function()}
* @private
*/
actionHandler_: null,
/**
* The current tooltip state.
* @type {{active: function(): boolean,
* autoshow: !UserPodCustomIcon.TooltipAutoshowState,
* hover: !UserPodCustomIcon.HoverState,
* text: string}}
* @private
*/
tooltipState_: {
/**
* Utility method for determining whether the tooltip is active, either as
* a result of hover state or being autoshown.
* @return {boolean}
*/
active: function() {
return this.autoshow == UserPodCustomIcon.TooltipAutoshowState.ACTIVE ||
this.hover == UserPodCustomIcon.HoverState.HOVER_TOOLTIP;
},
/**
* @type {!UserPodCustomIcon.TooltipAutoshowState}
*/
autoshow: UserPodCustomIcon.TooltipAutoshowState.DISABLED,
/**
* @type {!UserPodCustomIcon.HoverState}
*/
hover: UserPodCustomIcon.HoverState.NO_HOVER,
/**
* The tooltip text.
* @type {string}
*/
text: ''
},
/** @override */
decorate: function() {
this.iconElement.addEventListener(
'mouseover',
this.updateHoverState_.bind(this,
UserPodCustomIcon.HoverState.HOVER));
this.iconElement.addEventListener(
'mouseout',
this.updateHoverState_.bind(this,
UserPodCustomIcon.HoverState.NO_HOVER));
this.iconElement.addEventListener('mousedown',
this.handleMouseDown_.bind(this));
this.iconElement.addEventListener('click',
this.handleClick_.bind(this));
this.iconElement.addEventListener('keydown',
this.handleKeyDown_.bind(this));
// When the icon is focused using mouse, there should be no outline shown.
// Preventing default mousedown event accomplishes this.
this.iconElement.addEventListener('mousedown', function(e) {
e.preventDefault();
});
},
/**
* Getter for the icon element's div.
* @return {HTMLDivElement}
*/
get iconElement() {
return this.querySelector('.custom-icon');
},
/**
* Updates the icon element class list to properly represent the provided
* icon.
* @param {!string} id The id of the icon that should be shown. Should be
* one of the ids listed in {@code UserPodCustomIcon.ICONS}.
*/
setIcon: function(id) {
this.iconId_ = id;
UserPodCustomIcon.ICONS.forEach(function(icon) {
this.iconElement.classList.toggle(icon.class, id == icon.id);
}, this);
},
/**
* Sets the ARIA label for the icon.
* @param {!string} ariaLabel
*/
setAriaLabel: function(ariaLabel) {
this.iconElement.setAttribute('aria-label', ariaLabel);
},
/**
* Shows the icon.
*/
show: function() {
// Show the icon if the current iconId is valid.
var validIcon = false;
UserPodCustomIcon.ICONS.forEach(function(icon) {
validIcon = validIcon || this.iconId_ == icon.id;
}, this);
// Update password container width based on the visibility of the
// custom icon container.
var parentPod = this.getParentPod_();
if (parentPod) {
parentPod.passwordEntryContainerElement.classList.toggle(
'custom-icon-shown', validIcon);
}
this.hidden = !validIcon;
},
/**
* Updates the icon tooltip. If {@code autoshow} parameter is set the
* tooltip is immediatelly shown. If tooltip text is not set, the method
* ensures the tooltip gets hidden. If tooltip is shown prior to this call,
* it remains shown, but the tooltip text is updated.
* @param {!{text: string, autoshow: boolean}} tooltip The tooltip
* parameters.
*/
setTooltip: function(tooltip) {
this.iconElement.classList.toggle('icon-with-tooltip', !!tooltip.text);
this.updateTooltipAutoshowState_(
tooltip.autoshow ?
UserPodCustomIcon.TooltipAutoshowState.ENABLED :
UserPodCustomIcon.TooltipAutoshowState.DISABLED);
this.tooltipState_.text = tooltip.text;
this.updateTooltip_();
},
/**
* Sets up icon tabIndex attribute and handler for click and 'Enter' key
* down events.
* @param {?function()} callback If icon should be interactive, the
* function to get called on click and 'Enter' key down events. Should
* be null to make the icon non interactive.
*/
setInteractive: function(callback) {
this.iconElement.classList.toggle('interactive-custom-icon', !!callback);
// Update tabIndex property if needed.
if (!!this.actionHandler_ != !!callback) {
if (callback) {
this.iconElement.setAttribute('tabIndex',
UserPodTabOrder.POD_CUSTOM_ICON);
} else {
this.iconElement.removeAttribute('tabIndex');
}
}
// Set the new action handler.
this.actionHandler_ = callback;
},
/**
* Hides the icon and cleans its state.
*/
hide: function() {
this.hideTooltip_();
this.clearUpdateHoverStateTimeout_();
this.clearUpdateTooltipAutoshowStateTimeout_();
this.setInteractive(null);
this.hidden = true;
},
/**
* Clears timeout for showing a tooltip if one is set. Used to cancel
* showing the tooltip when the user starts typing the password.
*/
cancelDelayedTooltipShow: function() {
this.updateTooltipAutoshowState_(
UserPodCustomIcon.TooltipAutoshowState.DISABLED);
this.clearUpdateHoverStateTimeout_();
},
/**
* Handles mouse down event in the icon element.
* @param {Event} e The mouse down event.
* @private
*/
handleMouseDown_: function(e) {
this.updateHoverState_(UserPodCustomIcon.HoverState.NO_HOVER);
this.updateTooltipAutoshowState_(
UserPodCustomIcon.TooltipAutoshowState.DISABLED);
// Stop the event propagation so in the case the click ends up on the
// user pod (outside the custom icon) auth is not attempted.
stopEventPropagation(e);
},
/**
* Handles click event on the icon element. No-op if
* {@code this.actionHandler_} is not set.
* @param {Event} e The click event.
* @private
*/
handleClick_: function(e) {
if (!this.actionHandler_)
return;
this.actionHandler_();
stopEventPropagation(e);
},
/**
* Handles key down event on the icon element. Only 'Enter' key is handled.
* No-op if {@code this.actionHandler_} is not set.
* @param {Event} e The key down event.
* @private
*/
handleKeyDown_: function(e) {
if (!this.actionHandler_ || e.key != 'Enter')
return;
this.actionHandler_(e);
stopEventPropagation(e);
},
/**
* Changes the tooltip hover state and updates tooltip visibility if needed.
* @param {!UserPodCustomIcon.HoverState} state
* @private
*/
updateHoverState_: function(state) {
this.clearUpdateHoverStateTimeout_();
this.sanitizeTooltipStateIfBubbleHidden_();
if (state == UserPodCustomIcon.HoverState.HOVER) {
if (this.tooltipState_.active()) {
this.tooltipState_.hover = UserPodCustomIcon.HoverState.HOVER_TOOLTIP;
} else {
this.updateHoverStateSoon_(
UserPodCustomIcon.HoverState.HOVER_TOOLTIP);
}
return;
}
if (state != UserPodCustomIcon.HoverState.NO_HOVER &&
state != UserPodCustomIcon.HoverState.HOVER_TOOLTIP) {
console.error('Invalid hover state ' + state);
return;
}
this.tooltipState_.hover = state;
this.updateTooltip_();
},
/**
* Sets up a timeout for updating icon hover state.
* @param {!UserPodCustomIcon.HoverState} state
* @private
*/
updateHoverStateSoon_: function(state) {
if (this.updateHoverStateTimeout_)
clearTimeout(this.updateHoverStateTimeout_);
this.updateHoverStateTimeout_ =
setTimeout(this.updateHoverState_.bind(this, state), 1000);
},
/**
* Clears a timeout for updating icon hover state if there is one set.
* @private
*/
clearUpdateHoverStateTimeout_: function() {
if (this.updateHoverStateTimeout_) {
clearTimeout(this.updateHoverStateTimeout_);
this.updateHoverStateTimeout_ = null;
}
},
/**
* Changes the tooltip autoshow state and changes tooltip visibility if
* needed.
* @param {!UserPodCustomIcon.TooltipAutoshowState} state
* @private
*/
updateTooltipAutoshowState_: function(state) {
this.clearUpdateTooltipAutoshowStateTimeout_();
this.sanitizeTooltipStateIfBubbleHidden_();
if (state == UserPodCustomIcon.TooltipAutoshowState.DISABLED) {
if (this.tooltipState_.autoshow != state) {
this.tooltipState_.autoshow = state;
this.updateTooltip_();
}
return;
}
if (this.tooltipState_.active()) {
if (this.tooltipState_.autoshow !=
UserPodCustomIcon.TooltipAutoshowState.ACTIVE) {
this.tooltipState_.autoshow =
UserPodCustomIcon.TooltipAutoshowState.DISABLED;
} else {
// If the tooltip is already automatically shown, the timeout for
// removing it should be reset.
this.updateTooltipAutoshowStateSoon_(
UserPodCustomIcon.TooltipAutoshowState.DISABLED);
}
return;
}
if (state == UserPodCustomIcon.TooltipAutoshowState.ENABLED) {
this.updateTooltipAutoshowStateSoon_(
UserPodCustomIcon.TooltipAutoshowState.ACTIVE);
} else if (state == UserPodCustomIcon.TooltipAutoshowState.ACTIVE) {
this.updateTooltipAutoshowStateSoon_(
UserPodCustomIcon.TooltipAutoshowState.DISABLED);
}
this.tooltipState_.autoshow = state;
this.updateTooltip_();
},
/**
* Sets up a timeout for updating tooltip autoshow state.
* @param {!UserPodCustomIcon.TooltipAutoshowState} state
* @private
*/
updateTooltipAutoshowStateSoon_: function(state) {
if (this.updateTooltipAutoshowStateTimeout_)
clearTimeout(this.updateTooltupAutoshowStateTimeout_);
var timeout =
state == UserPodCustomIcon.TooltipAutoshowState.DISABLED ?
5000 : 1000;
this.updateTooltipAutoshowStateTimeout_ =
setTimeout(this.updateTooltipAutoshowState_.bind(this, state),
timeout);
},
/**
* Clears the timeout for updating tooltip autoshow state if one is set.
* @private
*/
clearUpdateTooltipAutoshowStateTimeout_: function() {
if (this.updateTooltipAutoshowStateTimeout_) {
clearTimeout(this.updateTooltipAutoshowStateTimeout_);
this.updateTooltipAutoshowStateTimeout_ = null;
}
},
/**
* If tooltip bubble is hidden, this makes sure that hover and tooltip
* autoshow states are not the ones that imply an active tooltip.
* Used to handle a case where the tooltip bubble is hidden by an event that
* does not update one of the states (e.g. click outside the pod will not
* update tooltip autoshow state). Should be called before making
* tooltip state updates.
* @private
*/
sanitizeTooltipStateIfBubbleHidden_: function() {
if (!$('bubble').hidden)
return;
if (this.tooltipState_.hover ==
UserPodCustomIcon.HoverState.HOVER_TOOLTIP &&
this.tooltipState_.text) {
this.tooltipState_.hover = UserPodCustomIcon.HoverState.NO_HOVER;
this.clearUpdateHoverStateTimeout_();
}
if (this.tooltipState_.autoshow ==
UserPodCustomIcon.TooltipAutoshowState.ACTIVE) {
this.tooltipState_.autoshow =
UserPodCustomIcon.TooltipAutoshowState.DISABLED;
this.clearUpdateTooltipAutoshowStateTimeout_();
}
},
/**
* Returns whether the user pod to which the custom icon belongs is focused.
* @return {boolean}
* @private
*/
isParentPodFocused_: function() {
if ($('account-picker').hidden)
return false;
var parentPod = this.getParentPod_();
return parentPod && parentPod.parentNode.isFocused(parentPod);
},
/**
* Depending on {@code this.tooltipState_}, it updates tooltip visibility
* and text.
* @private
*/
updateTooltip_: function() {
var parentPod = this.getParentPod_();
if (this.hidden || !parentPod ||
parentPod.getPodStyle() != UserPod.Style.LARGE ||
!this.isParentPodFocused_()) {
return;
}
if (!this.tooltipState_.active() || !this.tooltipState_.text) {
this.hideTooltip_();
return;
}
// Show the tooltip bubble.
var bubbleContent = document.createElement('div');
bubbleContent.textContent = this.tooltipState_.text;
parentPod.showBubble(bubbleContent);
},
/**
* Hides the tooltip.
* @private
*/
hideTooltip_: function() {
$('bubble').hideForElement(this);
},
/**
* Gets the parent pod (may be null) of this custom icon.
* @return {?HTMLDivElement}
*/
getParentPod_: function() {
var parentPod = this.parentNode;
while (parentPod && !parentPod.classList.contains('pod'))
parentPod = parentPod.parentNode;
return parentPod;
}
};
/**
* Unique salt added to user image URLs to prevent caching. Dictionary with
* user names as keys.
* @type {Object}
*/
UserPod.userImageSalt_ = {};
UserPod.prototype = {
__proto__: HTMLDivElement.prototype,
/**
* Whether click on the pod can issue a user click auth attempt. The
* attempt can be issued iff the pod was focused when the click
* started (i.e. on mouse down event).
* @type {boolean}
* @private
*/
userClickAuthAllowed_: false,
/**
* Whether the user has recently authenticated with fingerprint.
* @type {boolean}
* @private
*/
fingerprintAuthenticated_: false,
/**
* True iff the pod can display the pin keyboard. The pin keyboard may not
* always be displayed even if this is true, ie, if the virtual keyboard is
* also being displayed.
*/
pinEnabled: false,
/**
* If set, a function which hides a persistent detachable base warning
* bubble. This will be set if a detachable base warning bubble is shown for
* this pod.
* @type {?function()}
* @private
*/
detachableBaseWarningBubbleHider_: null,
/** @override */
decorate: function() {
this.tabIndex = UserPodTabOrder.POD_INPUT;
this.actionBoxAreaElement.tabIndex = UserPodTabOrder.POD_INPUT;
this.addEventListener('keydown', this.handlePodKeyDown_.bind(this));
this.addEventListener('click', this.handleClickOnPod_.bind(this));
this.addEventListener('mousedown', this.handlePodMouseDown_.bind(this));
if (this.pinKeyboard) {
this.pinKeyboard.passwordElement = this.passwordElement;
this.pinKeyboard.addEventListener('pin-change',
this.handleInputChanged_.bind(this));
this.pinKeyboard.tabIndex = UserPodTabOrder.PIN_KEYBOARD;
}
this.actionBoxAreaElement.addEventListener('mousedown',
stopEventPropagation);
this.actionBoxAreaElement.addEventListener('click',
this.handleActionAreaButtonClick_.bind(this));
this.actionBoxAreaElement.addEventListener('keydown',
this.handleActionAreaButtonKeyDown_.bind(this));
this.actionBoxMenuTitleElement.addEventListener('keydown',
this.handleMenuTitleElementKeyDown_.bind(this));
this.actionBoxMenuTitleElement.addEventListener('blur',
this.handleMenuTitleElementBlur_.bind(this));
this.actionBoxMenuRemoveElement.addEventListener('click',
this.handleRemoveCommandClick_.bind(this));
this.actionBoxMenuRemoveElement.addEventListener('keydown',
this.handleRemoveCommandKeyDown_.bind(this));
this.actionBoxMenuRemoveElement.addEventListener('blur',
this.handleRemoveCommandBlur_.bind(this));
this.actionBoxRemoveUserWarningButtonElement.addEventListener('click',
this.handleRemoveUserConfirmationClick_.bind(this));
this.actionBoxRemoveUserWarningButtonElement.addEventListener('keydown',
this.handleRemoveUserConfirmationKeyDown_.bind(this));
if (this.fingerprintIconElement) {
this.fingerprintIconElement.addEventListener(
'mouseover', this.handleFingerprintIconMouseOver_.bind(this));
this.fingerprintIconElement.addEventListener(
'mouseout', this.handleFingerprintIconMouseOut_.bind(this));
this.fingerprintIconElement.addEventListener(
'mousedown', stopEventPropagation);
}
var customIcon = this.customIconElement;
customIcon.parentNode.replaceChild(new UserPodCustomIcon(), customIcon);
},
/**
* Initializes the pod after its properties set and added to a pod row.
*/
initialize: function() {
this.passwordElement.addEventListener('keydown',
this.parentNode.handleKeyDown.bind(this.parentNode));
this.passwordElement.addEventListener('keypress',
this.handlePasswordKeyPress_.bind(this));
this.passwordElement.addEventListener('input',
this.handleInputChanged_.bind(this));
if (this.submitButton) {
this.submitButton.addEventListener('click',
this.handleSubmitButtonClick_.bind(this));
}
this.imageElement.addEventListener('load',
this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
this.smallPodImageElement.addEventListener(
'load',
this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
var initialAuthType = this.user.initialAuthType ||
AUTH_TYPE.OFFLINE_PASSWORD;
this.setAuthType(initialAuthType, null);
if (this.user.isActiveDirectory)
this.setAttribute('is-active-directory', '');
this.userClickAuthAllowed_ = false;
// Lazy load the assets needed for the polymer submit button.
var isLockScreen = (Oobe.getInstance().displayType == DISPLAY_TYPE.LOCK);
if (cr.isChromeOS && isLockScreen &&
!cr.ui.login.ResourceLoader.alreadyLoadedAssets(
'custom-elements-user-pod')) {
cr.ui.login.ResourceLoader.registerAssets({
id: 'custom-elements-user-pod',
html: [{ url: 'custom_elements_user_pod.html' }]
});
cr.ui.login.ResourceLoader.loadAssetsOnIdle('custom-elements-user-pod');
}
},
/**
* Whether the user pod is disabled.
* @type {boolean}
*/
disabled_: false,
get disabled() {
return this.disabled_;
},
set disabled(value) {
this.disabled_ = value;
this.querySelectorAll('button,input').forEach(function(element) {
element.disabled = value
});
this.tabIndex = value ? -1 : UserPodTabOrder.POD_INPUT;
this.actionBoxAreaElement.tabIndex =
value ? -1 : UserPodTabOrder.POD_INPUT;
// Special handling for submit button - the submit button should be
// enabled only if there is the password value set.
var submitButton = this.submitButton;
if (submitButton)
submitButton.disabled = value || !this.passwordElement.value;
},
/**
* Resets tab order for pod elements to its initial state.
*/
resetTabOrder: function() {
// Note: the |mainInput| can be the pod itself.
this.mainInput.tabIndex = -1;
this.tabIndex = UserPodTabOrder.POD_INPUT;
},
/**
* Handles keypress event (i.e. any textual input) on password input.
* @param {Event} e Keypress Event object.
* @private
*/
handlePasswordKeyPress_: function(e) {
// When tabbing from the system tray a tab key press is received. Suppress
// this so as not to type a tab character into the password field.
if (e.keyCode == 9) {
e.preventDefault();
return;
}
this.customIconElement.cancelDelayedTooltipShow();
},
/**
* Handles a click event on submit button.
* @param {Event} e Click event.
*/
handleSubmitButtonClick_: function(e) {
this.parentNode.setActivatedPod(this, e);
},
/**
* Top edge margin number of pixels.
* @type {?number}
*/
set top(top) {
this.style.top = cr.ui.toCssPx(top);
},
/**
* Top edge margin number of pixels.
*/
get top() {
return parseInt(this.style.top);
},
/**
* Left edge margin number of pixels.
* @type {?number}
*/
set left(left) {
this.style.left = cr.ui.toCssPx(left);
},
/**
* Left edge margin number of pixels.
*/
get left() {
return parseInt(this.style.left);
},
/**
* Height number of pixels.
*/
get height() {
return this.offsetHeight;
},
/**
* Gets image element.
* @type {!HTMLImageElement}
*/
get imageElement() {
return this.querySelector('.user-image');
},
/**
* Gets animated image element.
* @type {!HTMLImageElement}
*/
get animatedImageElement() {
return this.querySelector('.user-image.animated-image');
},
/**
* Gets name element.
* @type {!HTMLDivElement}
*/
get nameElement() {
return this.querySelector('.name');
},
/**
* Gets image element of the small pod.
* @type {!HTMLImageElement}
*/
get smallPodImageElement() {
return this.querySelector('.user-image.small-pod-image');
},
/**
* Gets animated image element.
* @type {!HTMLImageElement}
*/
get smallPodAnimatedImageElement() {
return this.querySelector('.user-image.small-pod-image.animated-image');
},
/**
* Gets name element of the small pod.
* @type {!HTMLDivElement}
*/
get smallPodNameElement() {
return this.querySelector('.small-pod-name');
},
/**
* Gets reauth name hint element.
* @type {!HTMLDivElement}
*/
get reauthNameHintElement() {
return this.querySelector('.reauth-name-hint');
},
/**
* Gets the container holding the password field.
* @type {!HTMLInputElement}
*/
get passwordEntryContainerElement() {
return this.querySelector('.password-entry-container');
},
/**
* Gets password field.
* @type {!HTMLInputElement}
*/
get passwordElement() {
return this.querySelector('.password');
},
/**
* Gets submit button.
* @type {!HTMLInputElement}
*/
get submitButton() {
return this.querySelector('.submit-button');
},
/**
* Gets the password label, which is used to show a message where the
* password field is normally.
* @type {!HTMLInputElement}
*/
get passwordLabelElement() {
return this.querySelector('.password-label');
},
get pinContainer() {
return this.querySelector('.pin-container');
},
/**
* Gets the pin-keyboard of the pod.
* @type {!HTMLElement}
*/
get pinKeyboard() {
return this.querySelector('pin-keyboard');
},
/**
* Gets user online sign in hint element.
* @type {!HTMLDivElement}
*/
get reauthWarningElement() {
return this.querySelector('.reauth-hint-container');
},
/**
* Gets the container holding the launch app button.
* @type {!HTMLButtonElement}
*/
get launchAppButtonContainerElement() {
return this.querySelector('.launch-app-button-container');
},
/**
* Gets launch app button.
* @type {!HTMLButtonElement}
*/
get launchAppButtonElement() {
return this.querySelector('.launch-app-button');
},
/**
* Gets action box area.
* @type {!HTMLInputElement}
*/
get actionBoxAreaElement() {
return this.querySelector('.action-box-area');
},
/**
* Gets user type icon area.
* @type {!HTMLDivElement}
*/
get userTypeIconAreaElement() {
return this.querySelector('.user-type-icon-area');
},
/**
* Gets user type bubble like multi-profiles policy restriction message.
* @type {!HTMLDivElement}
*/
get userTypeBubbleElement() {
return this.querySelector('.user-type-bubble');
},
/**
* Gets action box menu.
* @type {!HTMLDivElement}
*/
get actionBoxMenu() {
return this.querySelector('.action-box-menu');
},
/**
* Gets action box menu title (user name and email).
* @type {!HTMLDivElement}
*/
get actionBoxMenuTitleElement() {
return this.querySelector('.action-box-menu-title');
},
/**
* Gets action box menu title, user name item.
* @type {!HTMLSpanElement}
*/
get actionBoxMenuTitleNameElement() {
return this.querySelector('.action-box-menu-title-name');
},
/**
* Gets action box menu title, user email item.
* @type {!HTMLSpanElement}
*/
get actionBoxMenuTitleEmailElement() {
return this.querySelector('.action-box-menu-title-email');
},
/**
* Gets action box menu, remove user command item div.
* @type {!HTMLInputElement}
*/
get actionBoxMenuRemoveElement() {
return this.querySelector('.action-box-menu-remove');
},
/**
* Gets action box menu, remove user command item div.
* @type {!HTMLInputElement}
*/
get actionBoxRemoveUserWarningElement() {
return this.querySelector('.action-box-remove-user-warning');
},
/**
* Gets action box menu, remove user command item div.
* @type {!HTMLInputElement}
*/
get actionBoxRemoveUserWarningButtonElement() {
return this.querySelector('.remove-warning-button');
},
/**
* Gets the custom icon. This icon is normally hidden, but can be shown
* using the chrome.screenlockPrivate API.
* @type {!HTMLDivElement}
*/
get customIconElement() {
return this.querySelector('.custom-icon-container');
},
/**
* Gets the elements used for statistics display.
* @type {Object.<string, !HTMLDivElement>}
*/
get statsMapElements() {
return {
'BrowsingHistory':
this.querySelector('.action-box-remove-user-warning-history'),
'Passwords':
this.querySelector('.action-box-remove-user-warning-passwords'),
'Bookmarks':
this.querySelector('.action-box-remove-user-warning-bookmarks'),
'Autofill':
this.querySelector('.action-box-remove-user-warning-autofill')
}
},
/**
* Gets the fingerprint icon area.
* @type {!HTMLDivElement}
*/
get fingerprintIconElement() {
return this.querySelector('.fingerprint-icon-container');
},
/**
* Returns true if it's a public session pod.
* @type {boolean}
*/
get isPublicSessionPod() {
return this.classList.contains('public-account');
},
/**
* Sets the pod style.
* @param {UserPod.Style} style Style set to the pod.
*/
setPodStyle: function(style) {
switch (style) {
case UserPod.Style.LARGE:
this.querySelector('.large-pod').hidden = false;
this.querySelector('.small-pod').hidden = true;
break;
case UserPod.Style.SMALL:
this.querySelector('.large-pod').hidden = true;
this.querySelector('.small-pod').hidden = false;
this.querySelector('.small-pod').classList.remove('extra-small');
break;
case UserPod.Style.EXTRA_SMALL:
this.querySelector('.large-pod').hidden = true;
this.querySelector('.small-pod').hidden = false;
this.querySelector('.small-pod').classList.add('extra-small');
break;
default:
console.error("Attempt to set an invalid pod style.");
break;
}
},
/**
* Gets the pod style.
* @return {UserPod.Style} Pod style.
*/
getPodStyle: function() {
if (this.querySelector('.small-pod').hidden)
return UserPod.Style.LARGE;
if (this.querySelector('.small-pod').classList.contains('extra-small'))
return UserPod.Style.EXTRA_SMALL;
return UserPod.Style.SMALL;
},
/**
* Updates the user pod element.
*/
update: function() {
var animatedImageSrc = 'chrome://userimage/' + this.user.username +
'?id=' + UserPod.userImageSalt_[this.user.username];
var imageSrc = animatedImageSrc + '&frame=0';
this.imageElement.src = imageSrc;
this.animatedImageElement.src = animatedImageSrc;
this.smallPodImageElement.src = imageSrc;
this.smallPodAnimatedImageElement.src = animatedImageSrc;
this.nameElement.textContent = this.user_.displayName;
this.smallPodNameElement.textContent = this.user_.displayName;
this.reauthNameHintElement.textContent = this.user_.displayName;
this.classList.toggle('signed-in', this.user_.signedIn);
if (this.isAuthTypeUserClick)
this.passwordLabelElement.textContent = this.authValue;
this.updateActionBoxArea();
this.customizeUserPodPerUserType();
this.updateAriaLabels_();
},
updateActionBoxArea: function() {
if (this.user_.publicAccount || this.user_.isApp) {
this.actionBoxAreaElement.hidden = true;
return;
}
this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
this.user_.displayName;
this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
this.actionBoxMenuTitleEmailElement.hidden =
this.user_.legacySupervisedUser;
},
customizeUserPodPerUserType: function() {
if (this.user_.childUser && !this.user_.isDesktopUser) {
this.setUserPodIconType('child');
} else if (this.user_.legacySupervisedUser && !this.user_.isDesktopUser) {
this.setUserPodIconType('legacySupervised');
this.classList.add('legacy-supervised');
} else if (this.multiProfilesPolicyApplied) {
// Mark user pod as not focusable which in addition to the grayed out
// filter makes it look in disabled state.
this.classList.add('multiprofiles-policy-applied');
this.setUserPodIconType('policy');
if (this.user.multiProfilesPolicy ==
MULTI_PROFILE_USER_BEHAVIOR.PRIMARY_ONLY) {
this.querySelector('.mp-policy-primary-only-msg').hidden = false;
} else if (this.user.multiProfilesPolicy ==
MULTI_PROFILE_USER_BEHAVIOR.OWNER_PRIMARY_ONLY) {
this.querySelector('.mp-owner-primary-only-msg').hidden = false;
} else {
this.querySelector('.mp-policy-not-allowed-msg').hidden = false;
}
} else if (this.user_.isApp) {
this.setUserPodIconType('app');
}
},
/**
* Updates ARIA labels and sets hidden states. All updates related to ARIA
* should go here.
* @private
*/
updateAriaLabels_: function() {
this.setAttribute('aria-label', this.user_.displayName);
this.querySelector('.password-container')
.setAttribute(
'aria-label',
loadTimeData.getStringF(
'passwordFieldAccessibleName', this.user_.emailAddress));
this.actionBoxAreaElement.setAttribute(
'aria-label',
loadTimeData.getStringF(
'podMenuButtonAccessibleName', this.user_.emailAddress));
this.actionBoxMenuRemoveElement.setAttribute(
'aria-label',
loadTimeData.getString('podMenuRemoveItemAccessibleName'));
},
isPinReady: function() {
return this.pinKeyboard && this.pinKeyboard.offsetHeight > 0;
},
set showError(visible) {
if (this.submitButton)
this.submitButton.classList.toggle('error-shown', visible);
},
updatePinClass_: function(element, enable) {
element.classList.toggle('pin-enabled', enable);
element.classList.toggle('pin-disabled', !enable);
},
setPinVisibility: function(visible) {
if (this.isPinShown() == visible)
return;
// Do not show pin if virtual keyboard is there.
if (visible && Oobe.getInstance().virtualKeyboardShown)
return;
// Do not show pin keyboard if the pod does not have pin enabled.
if (visible && !this.pinEnabled)
return;
// Do not show pin keyboard if the pod is not in large style.
if (visible && this.getPodStyle() != UserPod.Style.LARGE)
return;
var elements = this.getElementsByClassName('pin-tag');
for (var i = 0; i < elements.length; ++i)
this.updatePinClass_(elements[i], visible);
this.updatePinClass_(this, visible);
// Set the focus to the input element after showing/hiding pin keyboard.
this.mainInput.focus();
// Change the password placeholder based on pin keyboard visibility.
this.passwordElement.placeholder = loadTimeData.getString(visible ?
'pinKeyboardPlaceholderPinPassword' : 'passwordHint');
// Adjust the vertical position based on the pin keyboard visibility.
var podHeight = visible ? CROS_POD_HEIGHT_WITH_PIN : CROS_POD_HEIGHT;
this.top = ($('pod-row').screenSize.height - podHeight) / 2;
},
isPinShown: function() {
return this.classList.contains('pin-enabled');
},
setUserPodIconType: function(userTypeClass) {
this.userTypeIconAreaElement.classList.add(userTypeClass);
// TODO(wzang): Evaluate all icon types other than supervised user and
// switch them to badges per the design spec.
this.userTypeIconAreaElement.hidden = true;
},
isFingerprintIconShown: function() {
return this.fingerprintIconElement && !this.fingerprintIconElement.hidden;
},
/**
* The user that this pod represents.
* @type {!Object}
*/
user_: undefined,
get user() {
return this.user_;
},
set user(userDict) {
this.user_ = userDict;
this.update();
},
/**
* Returns true if multi-profiles sign in is currently active and this
* user pod is restricted per policy.
* @type {boolean}
*/
get multiProfilesPolicyApplied() {
var isMultiProfilesUI =
(Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING);
return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed;
},
/**
* Gets main input element.
* @type {(HTMLButtonElement|HTMLInputElement)}
*/
get mainInput() {
if (this.isAuthTypePassword) {
return this.passwordElement;
} else if (this.isAuthTypeOnlineSignIn) {
return this;
} else if (this.isAuthTypeUserClick) {
return this.passwordLabelElement;
}
},
/**
* Whether action box button is in active state.
* @type {boolean}
*/
get isActionBoxMenuActive() {
return this.actionBoxAreaElement.classList.contains('active');
},
set isActionBoxMenuActive(active) {
if (active == this.isActionBoxMenuActive)
return;
if (active) {
this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
this.actionBoxRemoveUserWarningElement.hidden = true;
// Clear focus first if another pod is focused.
if (!this.parentNode.isFocused(this)) {
this.parentNode.focusPod(undefined, true);
this.actionBoxAreaElement.focus();
}
// Hide user-type-bubble.
this.userTypeBubbleElement.classList.remove('bubble-shown');
this.actionBoxAreaElement.classList.add('active');
// Invisible focus causes ChromeVox to read user name and email.
this.actionBoxMenuTitleElement.tabIndex = UserPodTabOrder.POD_MENU_ITEM;
this.actionBoxMenuTitleElement.focus();
} else {
this.actionBoxAreaElement.classList.remove('active');
this.actionBoxAreaElement.classList.remove('menu-moved-up');
this.actionBoxMenu.classList.remove('menu-moved-up');
}
},
/**
* Whether action box button is in hovered state.
* @type {boolean}
*/
get isActionBoxMenuHovered() {
return this.actionBoxAreaElement.classList.contains('hovered');
},
set isActionBoxMenuHovered(hovered) {
if (hovered == this.isActionBoxMenuHovered)
return;
if (hovered) {
this.actionBoxAreaElement.classList.add('hovered');
this.classList.add('hovered');
} else {
if (this.multiProfilesPolicyApplied)
this.userTypeBubbleElement.classList.remove('bubble-shown');
this.actionBoxAreaElement.classList.remove('hovered');
this.classList.remove('hovered');
}
},
/**
* Set the authentication type for the pod.
* @param {number} An auth type value defined in the AUTH_TYPE enum.
* @param {string} authValue The initial value used for the auth type.
*/
setAuthType: function(authType, authValue) {
this.authType_ = authType;
this.authValue_ = authValue;
this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]);
this.update();
this.reset(this.parentNode.isFocused(this));
},
/**
* The auth type of the user pod. This value is one of the enum
* values in AUTH_TYPE.
* @type {number}
*/
get authType() {
return this.authType_;
},
/**
* The initial value used for the pod's authentication type.
* eg. a prepopulated password input when using password authentication.
*/
get authValue() {
return this.authValue_;
},
/**
* True if the the user pod uses a password to authenticate.
* @type {bool}
*/
get isAuthTypePassword() {
return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD ||
this.authType_ == AUTH_TYPE.FORCE_OFFLINE_PASSWORD;
},
/**
* True if the the user pod uses a user click to authenticate.
* @type {bool}
*/
get isAuthTypeUserClick() {
return this.authType_ == AUTH_TYPE.USER_CLICK;
},
/**
* True if the the user pod uses a online sign in to authenticate.
* @type {bool}
*/
get isAuthTypeOnlineSignIn() {
return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN;
},
/**
* Updates the image element of the user.
*/
updateUserImage: function() {
UserPod.userImageSalt_[this.user.username] = new Date().getTime();
this.update();
},
/**
* Focuses on input element.
* @param {boolean?} opt_ensureFocus If true, keep trying to focus until a
* focus change event is raised.
*/
focusInput: function(opt_ensureFocus) {
// If |opt_ensureFocus| is set, keep setting the focus until we get a
// global focus change event. Sometimes focus requests are ignored while
// loading the page. See crbug.com/725622.
if (opt_ensureFocus) {
var INTERVAL_REPEAT_MS = 10;
var input = this.mainInput;
var intervalId = setInterval(function() {
input.focus();
}, INTERVAL_REPEAT_MS);
window.addEventListener('focus', function refocus() {
if (document.activeElement != input)
return;
window.removeEventListener('focus', refocus);
window.clearInterval(intervalId);
}, true);
}
// Move tabIndex from the whole pod to the main input.
// Note: the |mainInput| can be the pod itself.
this.tabIndex = -1;
this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
this.mainInput.focus();
},
/**
* Activates the pod.
* @param {Event} e Event object.
* @return {boolean} True if activated successfully.
*/
activate: function(e) {
if (this.isAuthTypeOnlineSignIn) {
this.showSigninUI();
} else if (this.isAuthTypeUserClick) {
Oobe.disableSigninUI();
this.classList.toggle('signing-in', true);
chrome.send('attemptUnlock', [this.user.username]);
} else if (this.isAuthTypePassword) {
if (this.fingerprintAuthenticated_) {
this.fingerprintAuthenticated_ = false;
return true;
}
var pinValue = this.pinKeyboard ? this.pinKeyboard.value : '';
var password = this.passwordElement.value || pinValue;
if (!password)
return false;
Oobe.disableSigninUI();
chrome.send('authenticateUser', [
this.user.username, password, this.isPinShown() && !isNaN(password)
]);
} else {
console.error('Activating user pod with invalid authentication type: ' +
this.authType);
}
return true;
},
showSupervisedUserSigninWarning: function() {
// Legacy supervised user token has been invalidated.
// Make sure that pod is focused i.e. "Sign in" button is seen.
this.parentNode.focusPod(this);
var error = document.createElement('div');
var messageDiv = document.createElement('div');
messageDiv.className = 'error-message-bubble';
messageDiv.textContent =
loadTimeData.getString('supervisedUserExpiredTokenWarning');
error.appendChild(messageDiv);
$('bubble').showContentForElement(
this.reauthWarningElement,
cr.ui.Bubble.Attachment.TOP,
error,
this.reauthWarningElement.offsetWidth / 2,
4);
// Move warning bubble up if it overlaps the shelf.
var maxHeight =
cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping($('bubble'));
if (maxHeight < $('bubble').offsetHeight) {
$('bubble').showContentForElement(
this.reauthWarningElement,
cr.ui.Bubble.Attachment.BOTTOM,
error,
this.reauthWarningElement.offsetWidth / 2,
4);
}
},
/**
* Shows signin UI for this user.
*/
showSigninUI: function() {
if (this.user.legacySupervisedUser && !this.user.isDesktopUser) {
this.showSupervisedUserSigninWarning();
} else {
// Special case for multi-profiles sign in. We show users even if they
// are not allowed per policy. Restrict those users from starting GAIA.
if (this.multiProfilesPolicyApplied)
return;
this.parentNode.showSigninUI(this.user.emailAddress);
}
},
/**
* Returns the element that should be used as the anchor for error bubbles
* associated with the pod.
*
* @return {HTMLElement} The anchor for error bubbles.
* @private
*/
getBubbleAnchor_: function() {
var bubbleAnchor = this.getElementsByClassName('auth-container')[0];
if (!bubbleAnchor) {
console.error('auth-container not found!');
bubbleAnchor = this.mainInput;
}
return bubbleAnchor;
},
/**
* Shows a bubble under the auth-container of the user pod.
* @param {HTMLElement} content Content to show in bubble.
* @param {!{bubble: (HTMLElement|undefined),
* anchor: (HTMLElement|undefined),
* timeout: (number|undefined)}|undefined} opt_options The custom
* options describing how the bubble should be shown:
* <ul>
* <li>bubble: The element that hosts the bubble content.</li>
* <li>
* anchor: The element to which the bubble should be anchored.
* </li>
* <li>
* timeout: Amount of time in ms after which the bubble
* should be hidden. Note: this should only be used for
* {@code $('bubble')} bubble element. The timeout will get
* cleared if the bubble is shown again.
* </li>
* </ul>
* @return {function()} Function that, when called, hides the shown bubble.
*/
showBubble: function(content, opt_options) {
/** @const */ var BUBBLE_OFFSET = 25;
// -8 = 4(BUBBLE_POD_OFFSET) - 2(bubble margin)
// - 10(internal bubble adjustment)
var bubblePositioningPadding = -8;
var options = opt_options || {};
var bubble = options.bubble || $('bubble');
// Make sure bubble timeout is changed only for $('bubble') element.
if (options.timeout && bubble != $('bubble')) {
console.error('Timeout can be set only when showing #bubble element.');
return;
}
var bubbleAnchor = options.anchor || this.getBubbleAnchor_();
var attachment;
if (this.pinContainer && this.pinContainer.style.visibility == 'visible')
attachment = cr.ui.Bubble.Attachment.RIGHT;
else
attachment = cr.ui.Bubble.Attachment.BOTTOM;
// Cannot use cr.ui.LoginUITools.get* on bubble until it is attached to
// the element. getMaxHeight/Width rely on the correct up/left element
// side positioning that doesn't happen until bubble is attached.
var maxHeight =
cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(bubbleAnchor) -
bubbleAnchor.offsetHeight - BUBBLE_POD_OFFSET;
var maxWidth = cr.ui.LoginUITools.getMaxWidthToFit(bubbleAnchor) -
bubbleAnchor.offsetWidth - BUBBLE_POD_OFFSET;
// Change bubble visibility temporary to calculate height.
var bubbleVisibility = bubble.style.visibility;
bubble.style.visibility = 'hidden';
bubble.hidden = false;
// Now we need the bubble to have the new content before calculating
// size. Undefined |content| == reuse old content.
if (content !== undefined)
bubble.replaceContent(content);
// Get bubble size.
var bubbleOffsetHeight = parseInt(bubble.offsetHeight);
var bubbleOffsetWidth = parseInt(bubble.offsetWidth);
// Restore attributes.
bubble.style.visibility = bubbleVisibility;
bubble.hidden = true;
if (attachment == cr.ui.Bubble.Attachment.BOTTOM) {
// Move the bubble if it overlaps the shelf.
if (maxHeight < bubbleOffsetHeight)
attachment = cr.ui.Bubble.Attachment.TOP;
} else {
// Move the bubble if it doesn't fit screen.
if (maxWidth < bubbleOffsetWidth) {
bubblePositioningPadding = 2;
attachment = cr.ui.Bubble.Attachment.LEFT;
}
}
if (bubble == $('bubble'))
this.clearBubbleHideTimeout_();
var state = {shown: false, hidden: false};
var showBubbleCallback = function() {
this.removeEventListener('transitionend', showBubbleCallback);
// If the bubble was requested to be hidden while the transition was in
// progress, do not show the bubble.
if (state.hidden)
return;
state.shown = true;
bubble.showContentForElement(
bubbleAnchor, attachment, content, BUBBLE_OFFSET,
bubblePositioningPadding, true);
if (options.timeout != undefined) {
this.hideBubbleTimeout_ = setTimeout(() => {
this.hideBubbleTimeout_ = undefined;
bubble.hideForElement(bubbleAnchor);
}, options.timeout);
}
}.bind(this);
this.addEventListener('transitionend', showBubbleCallback);
ensureTransitionEndEvent(this);
return function() {
if (state.hidden)
return;
state.hidden = true;
if (state.shown)
bubble.hideForElement(bubbleAnchor);
};
},
/**
* Clears the timeout to hide a bubble, if a bubble timeout was set.
* @private
*/
clearBubbleHideTimeout_: function() {
if (this.hideBubbleTimeout_) {
clearTimeout(this.hideBubbleTimeout_);
this.hideBubbleTimeout_ = null;
}
},
/**
* Shows persistent bubble for detachable base change warning.
* @param {HTMLElement} content The bubble contens.
*/
showDetachableBaseWarningBubble: function(content) {
var anchor = this.getBubbleAnchor_();
if (!anchor)
return;
this.clearBubbleHideTimeout_();
$('bubble').hideForElement(anchor);
this.detachableBaseWarningBubbleHider_ = this.showBubble(
content, {bubble: $('bubble-persistent'), anchor: anchor});
},
/**
* If a peristent bubble for detachable base change warning is shown (and
* anchored at this pod), hides the bubble.
*/
hideDetachableBaseWarningBubble: function() {
if (this.detachableBaseWarningBubbleHider_) {
this.detachableBaseWarningBubbleHider_();
this.detachableBaseWarningBubbleHider_ = null;
}
},
/**
* Whether a detachable base warning bubble is being shown for this pod.
* @return {boolean}
*/
showingDetachableBaseWarningBubble: function() {
return this.detachableBaseWarningBubbleHider_ &&
!$('bubble-persistent').hidden &&
$('bubble-persistent').anchor == this.getBubbleAnchor_();
},
/**
* Resets the input field and updates the tab order of pod controls.
* @param {boolean} takeFocus If true, input field takes focus.
*/
reset: function(takeFocus) {
this.passwordElement.value = '';
if (this.pinKeyboard)
this.pinKeyboard.value = '';
this.updateInput_();
this.classList.toggle('signing-in', false);
if (takeFocus) {
if (!this.multiProfilesPolicyApplied) {
this.focusInput(
this.mainInput.tagName == 'INPUT' /*opt_ensureFocus*/);
}
}
else
this.resetTabOrder();
},
/**
* Removes a user using the correct identifier based on user type.
* @param {Object} user User to be removed.
*/
removeUser: function(user) {
chrome.send('removeUser',
[user.isDesktopUser ? user.profilePath : user.username]);
},
/**
* Handles a click event on action area button.
* @param {Event} e Click event.
*/
handleActionAreaButtonClick_: function(e) {
if (this.parentNode.disabled)
return;
this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
// Action area menu and error bubbles shouldn't appear together.
Oobe.clearErrors();
e.stopPropagation();
},
/**
* Handles a keydown event on action area button.
* @param {Event} e KeyDown event.
*/
handleActionAreaButtonKeyDown_: function(e) {
if (this.disabled)
return;
switch (e.key) {
case 'Enter':
case ' ':
if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive) {
this.isActionBoxMenuActive = true;
Oobe.clearErrors();
}
e.stopPropagation();
break;
case 'ArrowUp':
case 'ArrowDown':
if (this.isActionBoxMenuActive) {
this.actionBoxMenuRemoveElement.tabIndex =
UserPodTabOrder.POD_MENU_ITEM;
this.actionBoxMenuRemoveElement.focus();
}
e.stopPropagation();
break;
// Ignore these two, so ChromeVox hotkeys don't close the menu before
// they can navigate through it.
case 'Shift':
case 'Meta':
break;
case 'Escape':
this.actionBoxAreaElement.focus();
this.isActionBoxMenuActive = false;
e.stopPropagation();
break;
case 'Tab':
if (!this.parentNode.alwaysFocusSinglePod)
this.parentNode.focusPod();
default:
this.isActionBoxMenuActive = false;
break;
}
},
/**
* Handles a keydown event on menu title.
* @param {Event} e KeyDown event.
*/
handleMenuTitleElementKeyDown_: function(e) {
if (this.disabled)
return;
if (e.key != 'Tab') {
this.handleActionAreaButtonKeyDown_(e);
return;
}
if (e.shiftKey == false) {
if (this.actionBoxMenuRemoveElement.hidden) {
this.isActionBoxMenuActive = false;
} else {
this.actionBoxMenuRemoveElement.tabIndex =
UserPodTabOrder.POD_MENU_ITEM;
this.actionBoxMenuRemoveElement.focus();
e.preventDefault();
}
} else {
this.isActionBoxMenuActive = false;
this.focusInput();
e.preventDefault();
}
},
/**
* Handles a blur event on menu title.
* @param {Event} e Blur event.
*/
handleMenuTitleElementBlur_: function(e) {
if (this.disabled)
return;
this.actionBoxMenuTitleElement.tabIndex = -1;
},
/**
* Handles a click event on remove user command.
* @param {Event} e Click event.
*/
handleRemoveCommandClick_: function(e) {
this.showRemoveWarning_();
},
/**
* Move the action box menu up if needed.
*/
moveActionMenuUpIfNeeded_: function() {
// Skip checking (computationally expensive) if already moved up.
if (this.actionBoxMenu.classList.contains('menu-moved-up'))
return;
// Move up the menu if it overlaps shelf.
var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(
this.actionBoxMenu, true);
var actualHeight = parseInt(
window.getComputedStyle(this.actionBoxMenu).height);
if (maxHeight < actualHeight) {
this.actionBoxMenu.classList.add('menu-moved-up');
this.actionBoxAreaElement.classList.add('menu-moved-up');
}
},
/**
* Shows remove user warning. Used for legacy supervised users
* and non-device-owner on CrOS, and for all users on desktop.
*/
showRemoveWarning_: function() {
this.actionBoxMenuRemoveElement.hidden = true;
this.actionBoxRemoveUserWarningElement.hidden = false;
if (!this.user.isDesktopUser) {
this.moveActionMenuUpIfNeeded_();
if (!this.user.legacySupervisedUser) {
this.querySelector(
'.action-box-remove-user-warning-text').style.display = 'none';
this.querySelector(
'.action-box-remove-user-warning-table-nonsync').style.display
= 'none';
var message = loadTimeData.getString('removeNonOwnerUserWarningText');
this.updateRemoveNonOwnerUserWarningMessage_(this.user.profilePath,
message);
}
} else {
// Show extra statistics information for desktop users
this.querySelector(
'.action-box-remove-non-owner-user-warning-text').hidden = true;
this.RemoveWarningDialogSetMessage_();
// set a global handler for the callback
window.updateRemoveWarningDialog =
this.updateRemoveWarningDialog_.bind(this);
var is_synced_user = this.user.emailAddress !== "";
if (!is_synced_user) {
chrome.send('removeUserWarningLoadStats', [this.user.profilePath]);
}
}
chrome.send('logRemoveUserWarningShown');
},
/**
* Refresh the statistics in the remove user warning dialog.
* @param {string} profilePath The filepath of the URL (must be verified).
* @param {Object} profileStats Statistics associated with profileURL.
*/
updateRemoveWarningDialog_: function(profilePath, profileStats) {
if (profilePath !== this.user.profilePath)
return;
var stats_elements = this.statsMapElements;
// Update individual statistics
for (var key in profileStats) {
if (stats_elements.hasOwnProperty(key)) {
stats_elements[key].textContent = profileStats[key].count;
}
}
},
/**
* Set the new message in the dialog.
*/
RemoveWarningDialogSetMessage_: function() {
var is_synced_user = this.user.emailAddress !== "";
message = loadTimeData.getString(
is_synced_user ? 'removeUserWarningTextSync' :
'removeUserWarningTextNonSync');
this.updateRemoveWarningDialogSetMessage_(this.user.profilePath,
message);
},
/**
* Refresh the message in the remove user warning dialog.
* @param {string} profilePath The filepath of the URL (must be verified).
* @param {string} message The message to be written.
* @param {number|string=} count The number or string to replace $1 in
* |message|. Can be omitted if $1 is not present in |message|.
*/
updateRemoveWarningDialogSetMessage_: function(
profilePath, message, count) {
if (profilePath !== this.user.profilePath)
return;
// Add localized messages where $1 will be replaced with
// <span class="total-count"></span> and $2 will be replaced with
// <span class="email"></span>.
var element = this.querySelector('.action-box-remove-user-warning-text');
element.textContent = '';
messageParts = message.split(/(\$[12])/);
var numParts = messageParts.length;
for (var j = 0; j < numParts; j++) {
if (messageParts[j] === '$1') {
var elementToAdd = document.createElement('span');
elementToAdd.classList.add('total-count');
elementToAdd.textContent = count;
element.appendChild(elementToAdd);
} else if (messageParts[j] === '$2') {
var elementToAdd = document.createElement('span');
elementToAdd.classList.add('email');
elementToAdd.textContent = this.user.emailAddress;
element.appendChild(elementToAdd);
} else {
element.appendChild(document.createTextNode(messageParts[j]));
}
}
this.moveActionMenuUpIfNeeded_();
},
/**
* Update the message in the "remove non-owner user warning" dialog on CrOS.
* @param {string} profilePath The filepath of the URL (must be verified).
* @param (string) message The message to be written.
*/
updateRemoveNonOwnerUserWarningMessage_: function(profilePath, message) {
if (profilePath !== this.user.profilePath)
return;
// Add localized messages where $1 will be replaced with
// <span class="email"></span>.
var element = this.querySelector(
'.action-box-remove-non-owner-user-warning-text');
element.textContent = '';
messageParts = message.split(/(\$[1])/);
var numParts = messageParts.length;
for (var j = 0; j < numParts; j++) {
if (messageParts[j] == '$1') {
var elementToAdd = document.createElement('span');
elementToAdd.classList.add('email');
elementToAdd.textContent = this.user.emailAddress;
element.appendChild(elementToAdd);
} else {
element.appendChild(document.createTextNode(messageParts[j]));
}
}
this.moveActionMenuUpIfNeeded_();
},
/**
* Handles a click event on remove user confirmation button.
* @param {Event} e Click event.
*/
handleRemoveUserConfirmationClick_: function(e) {
if (this.isActionBoxMenuActive) {
this.isActionBoxMenuActive = false;
this.removeUser(this.user);
e.stopPropagation();
}
},
/**
* Handles mouseover event on fingerprint icon.
* @param {Event} e MouseOver event.
*/
handleFingerprintIconMouseOver_: function(e) {
var bubbleContent = document.createElement('div');
bubbleContent.textContent =
loadTimeData.getString('fingerprintIconMessage');
this.passwordElement.placeholder =
loadTimeData.getString('fingerprintHint');
/** @const */ var BUBBLE_OFFSET = 25;
/** @const */ var BUBBLE_PADDING = -8;
var attachment = this.isPinShown() ? cr.ui.Bubble.Attachment.RIGHT :
cr.ui.Bubble.Attachment.BOTTOM;
var bubbleAnchor = this.getBubbleAnchorForFingerprintIcon_();
$('bubble').showContentForElement(
bubbleAnchor, attachment, bubbleContent, BUBBLE_OFFSET,
BUBBLE_PADDING, true);
},
/**
* Handles mouseout event on fingerprint icon.
* @param {Event} e MouseOut event.
*/
handleFingerprintIconMouseOut_: function(e) {
var bubbleAnchor = this.getBubbleAnchorForFingerprintIcon_();
$('bubble').hideForElement(bubbleAnchor);
this.passwordElement.placeholder = loadTimeData.getString(
this.isPinShown() ? 'pinKeyboardPlaceholderPinPassword' :
'passwordHint');
},
/**
* Returns bubble anchor of the fingerprint icon.
* @return {!HTMLElement} Anchor element of the bubble.
*/
getBubbleAnchorForFingerprintIcon_: function() {
var bubbleAnchor = this;
if (this.isPinShown())
bubbleAnchor = (this.getElementsByClassName('auth-container'))[0];
return bubbleAnchor;
},
/**
* Handles a keydown event on remove user confirmation button.
* @param {Event} e KeyDown event.
*/
handleRemoveUserConfirmationKeyDown_: function(e) {
if (!this.isActionBoxMenuActive)
return;
// Only handle pressing 'Enter' or 'Space', and let all other events
// bubble to the action box menu.
if (e.key == 'Enter' || e.key == ' ') {
this.isActionBoxMenuActive = false;
this.removeUser(this.user);
e.stopPropagation();
// Prevent default so that we don't trigger a 'click' event.
e.preventDefault();
}
},
/**
* Handles a keydown event on remove command.
* @param {Event} e KeyDown event.
*/
handleRemoveCommandKeyDown_: function(e) {
if (this.disabled)
return;
switch (e.key) {
case 'Enter':
e.preventDefault();
this.showRemoveWarning_();
e.stopPropagation();
break;
case 'ArrowUp':
case 'ArrowDown':
e.stopPropagation();
break;
// Ignore these two, so ChromeVox hotkeys don't close the menu before
// they can navigate through it.
case 'Shift':
case 'Meta':
break;
case 'Escape':
this.actionBoxAreaElement.focus();
this.isActionBoxMenuActive = false;
e.stopPropagation();
break;
default:
this.actionBoxAreaElement.focus();
this.isActionBoxMenuActive = false;
break;
}
},
/**
* Handles a blur event on remove command.
* @param {Event} e Blur event.
*/
handleRemoveCommandBlur_: function(e) {
if (this.disabled)
return;
this.actionBoxMenuRemoveElement.tabIndex = -1;
},
/**
* Handles mouse down event. It sets whether the user click auth will be
* allowed on the next mouse click event. The auth is allowed iff the pod
* was focused on the mouse down event starting the click.
* @param {Event} e The mouse down event.
*/
handlePodMouseDown_: function(e) {
// Only large pods have mouse down event.
if (this.getPodStyle() == UserPod.Style.LARGE)
this.userClickAuthAllowed_ = this.parentNode.isFocused(this);
},
/**
* Called when the input of the password element changes. Updates the submit
* button color and state and hides the error popup bubble.
*/
updateInput_: function() {
var isEmpty = this.passwordElement.value.length == 0;
if (this.submitButton) {
this.submitButton.disabled = isEmpty;
if (this.isFingerprintIconShown()) {
this.submitButton.hidden = isEmpty;
} else {
this.submitButton.hidden = false;
}
}
this.showError = false;
$('bubble').hide();
this.classList.toggle('input-present', !isEmpty);
},
/**
* Handles input event on the password element.
* @param {Event} e Input event.
*/
handleInputChanged_: function(e) {
this.updateInput_();
},
/**
* Handles click event on a user pod.
* @param {Event} e Click event.
*/
handleClickOnPod_: function(e) {
if (this.parentNode.disabled)
return;
// Click events on public session pods should only be handled by their
// overriding handler.
if (this.isPublicSessionPod)
return;
if (this.getPodStyle() != UserPod.Style.LARGE) {
$('pod-row').switchMainPod(this);
return;
}
Oobe.clearErrors();
if (!this.isActionBoxMenuActive) {
if (this.isAuthTypeOnlineSignIn) {
this.showSigninUI();
} else if (this.isAuthTypeUserClick && this.userClickAuthAllowed_) {
// Note that this.userClickAuthAllowed_ is set in mouse down event
// handler.
this.parentNode.setActivatedPod(this);
} else if (this.pinKeyboard &&
e.target == this.pinKeyboard.submitButton) {
// Sets the pod as activated if the submit button is clicked so that
// it simulates what the enter button does for the password/pin.
this.parentNode.setActivatedPod(this);
}
if (this.multiProfilesPolicyApplied)
this.userTypeBubbleElement.classList.add('bubble-shown');
// Prevent default so that we don't trigger 'focus' event and
// stop propagation so that the 'click' event does not bubble
// up and accidentally closes the bubble tooltip.
stopEventPropagation(e);
}
},
/**
* Handles keydown event for a user pod.
* @param {Event} e Key event.
*/
handlePodKeyDown_: function(e) {
if (this.getPodStyle() != UserPod.Style.LARGE) {
this.handleNonLargePodKeyDown_(e);
return;
}
if (!this.isAuthTypeUserClick || this.disabled)
return;
switch (e.key) {
case 'Enter':
case ' ':
if (this.parentNode.isFocused(this))
this.parentNode.setActivatedPod(this);
break;
}
},
/**
* Handles keydown event for a small or extra small user pod.
* @param {Event} e Key event.
*/
handleNonLargePodKeyDown_: function(e) {
switch (e.key) {
case 'Enter':
case ' ':
if ($('pod-row').isFocused(this))
$('pod-row').switchMainPod(this);
break;
}
}
};
/**
* Creates a public account user pod.
* @constructor
* @extends {UserPod}
*/
var PublicAccountUserPod = cr.ui.define(function() {
var node = UserPod();
var extras = $('public-account-user-pod-extras-template').children;
for (var i = 0; i < extras.length; ++i) {
var el = extras[i].cloneNode(true);
node.appendChild(el);
}
return node;
});
PublicAccountUserPod.prototype = {
__proto__: UserPod.prototype,
/**
* Keeps track of the pod's original position before it's expanded.
* @type {Object}
*/
lastPosition: {left: 'unset', top: 'unset'},
/**
* The Learn more dialog.
* @type {HTMLDivElement}
*/
learnMoreDialog_: undefined,
/**
* "Enter" button in expanded side pane.
* @type {!HTMLButtonElement}
*/
get enterButtonElement() {
return this.querySelector('.enter-button');
},
/**
* Boolean flag of whether the pod is showing the side pane. The flag
* controls whether 'expanded' class is added to the pod's class list and
* resets tab order because main input element changes when the 'expanded'
* state changes.
* @type {boolean}
*/
get expanded() {
return this.classList.contains('expanded');
},
set expanded(expanded) {
if (this.getPodStyle() != UserPod.Style.LARGE) {
console.error(
'Attempt to expand a public session pod when it is not large.');
return;
}
if (this.expanded == expanded)
return;
this.resetTabOrder();
this.classList.toggle('expanded', expanded);
if (expanded) {
// Show the advanced expanded pod directly if there are at least two
// recommended locales. This will be the case in multilingual
// environments where users are likely to want to choose among locales.
if (this.querySelector('.language-select').multipleRecommendedLocales)
this.classList.add('advanced');
} else {
this.classList.remove('advanced');
}
this.parentNode.handlePublicPodExpansion(this, expanded);
},
get advanced() {
return this.classList.contains('advanced');
},
/** @override */
get mainInput() {
if (this.expanded)
return this.querySelector('.monitoring-learn-more');
else
return this.nameElement;
},
/** @override */
decorate: function() {
UserPod.prototype.decorate.call(this);
this.classList.add('public-account');
this.nameElement.addEventListener('keydown', (function(e) {
if (e.key == 'Enter') {
this.parentNode.setActivatedPod(this, e);
// Stop this keydown event from bubbling up to PodRow handler.
e.stopPropagation();
// Prevent default so that we don't trigger a 'click' event on the
// newly focused "Enter" button.
e.preventDefault();
}
}).bind(this));
var learnMore = this.querySelector('.learn-more');
learnMore.addEventListener('mousedown', stopEventPropagation);
learnMore.addEventListener('click', this.handleLearnMoreEvent);
learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
var languageSelect = this.querySelector('.language-select');
languageSelect.tabIndex = UserPodTabOrder.POD_INPUT;
languageSelect.manuallyChanged = false;
languageSelect.addEventListener(
'change',
function() {
languageSelect.manuallyChanged = true;
this.getPublicSessionKeyboardLayouts_();
}.bind(this));
var keyboardSelect = this.querySelector('.keyboard-select');
keyboardSelect.tabIndex = UserPodTabOrder.POD_INPUT;
keyboardSelect.loadedLocale = null;
var languageAndInputLink = this.querySelector('.language-and-input');
languageAndInputLink.tabIndex = UserPodTabOrder.POD_INPUT;
languageAndInputLink.addEventListener(
'click', this.transitionToAdvanced_.bind(this));
var languageAndInputIcon =
this.querySelector('.language-and-input-dropdown');
languageAndInputIcon.addEventListener(
'click', this.transitionToAdvanced_.bind(this));
var monitoringLearnMore = this.querySelector('.monitoring-learn-more');
monitoringLearnMore.tabIndex = UserPodTabOrder.POD_INPUT;
monitoringLearnMore.addEventListener(
'click', this.onLearnMoreClicked_.bind(this));
this.enterButtonElement.tabIndex = UserPodTabOrder.POD_INPUT;
this.enterButtonElement.addEventListener('click', (function(e) {
this.enterButtonElement.disabled = true;
var locale = this.querySelector('.language-select').value;
var keyboardSelect = this.querySelector('.keyboard-select');
// The contents of |keyboardSelect| is updated asynchronously. If its
// locale does not match |locale|, it has not updated yet and the
// currently selected keyboard layout may not be applicable to |locale|.
// Do not return any keyboard layout in this case and let the backend
// choose a suitable layout.
var keyboardLayout =
keyboardSelect.loadedLocale == locale ? keyboardSelect.value : '';
chrome.send('launchPublicSession',
[this.user.username, locale, keyboardLayout]);
}).bind(this));
},
/** @override **/
initialize: function() {
UserPod.prototype.initialize.call(this);
id = this.user.username + '-keyboard';
this.querySelector('.keyboard-select-label').htmlFor = id;
this.querySelector('.keyboard-select').setAttribute('id', id);
var id = this.user.username + '-language';
this.querySelector('.language-select-label').htmlFor = id;
var languageSelect = this.querySelector('.language-select');
languageSelect.setAttribute('id', id);
this.populateLanguageSelect(this.user.initialLocales,
this.user.initialLocale,
this.user.initialMultipleRecommendedLocales);
},
/** @override **/
update: function() {
UserPod.prototype.update.call(this);
this.querySelector('.info').textContent =
loadTimeData.getStringF('publicAccountInfoFormat',
this.user_.enterpriseDisplayDomain);
if (this.querySelector('.full-name'))
this.querySelector('.full-name').textContent = this.user_.displayName;
},
/** @override */
focusInput: function() {
// Move tabIndex from the whole pod to the main input.
this.tabIndex = -1;
this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
this.mainInput.focus();
},
/** @override */
reset: function(takeFocus) {
if (!takeFocus)
this.expanded = false;
this.enterButtonElement.disabled = false;
UserPod.prototype.reset.call(this, takeFocus);
},
/** @override */
activate: function(e) {
if (!this.expanded) {
this.expanded = true;
this.focusInput();
}
return true;
},
/** @override */
handleClickOnPod_: function(e) {
if (this.parentNode.disabled)
return;
if (this.getPodStyle() != UserPod.Style.LARGE) {
$('pod-row').switchMainPod(this);
return;
}
Oobe.clearErrors();
this.parentNode.focusPod(this);
this.parentNode.setActivatedPod(this, e);
// Prevent default so that we don't trigger 'focus' event.
e.preventDefault();
},
/**
* Updates the display name shown on the pod.
* @param {string} displayName The new display name
*/
setDisplayName: function(displayName) {
this.user_.displayName = displayName;
this.update();
},
/**
* Handle mouse and keyboard events for the learn more button. Triggering
* the button causes information about public sessions to be shown.
* @param {Event} event Mouse or keyboard event.
*/
handleLearnMoreEvent: function(event) {
switch (event.type) {
// Show informaton on left click. Let any other clicks propagate.
case 'click':
if (event.button != 0)
return;
break;
// Show informaton when <Return> or <Space> is pressed. Let any other
// key presses propagate.
case 'keydown':
switch (event.keyCode) {
case 13: // Return.
case 32: // Space.
break;
default:
return;
}
break;
}
chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
stopEventPropagation(event);
},
/**
* Transition the expanded pod from the basic to the advanced view.
*/
transitionToAdvanced_: function() {
this.classList.add('advanced');
},
/**
* Show a dialog when user clicks on Learn more button.
*/
onLearnMoreClicked_: function() {
// Ignore if the Learn more dialog is already open.
if (this.learnMoreDialog_)
return;
var topContainer = document.querySelector('#scroll-container');
var dialogContainer =
topContainer.querySelector('.monitoring-dialog-container');
if (!dialogContainer) {
// Add a dummy parent element to enable different CSS settings.
dialogContainer = document.createElement('div');
dialogContainer.classList.add('monitoring-dialog-container');
topContainer.appendChild(dialogContainer);
}
dialogContainer.classList.toggle(
'advanced', this.classList.contains('advanced'));
var html = '';
var infoItems = ['publicAccountMonitoringInfoItem1',
'publicAccountMonitoringInfoItem2',
'publicAccountMonitoringInfoItem3',
'publicAccountMonitoringInfoItem4'];
for (item of infoItems) {
html += '<p class="cr-dialog-item">';
html += loadTimeData.getString(item);
html += '</p>';
}
var title = loadTimeData.getString('publicAccountMonitoringInfo');
this.learnMoreDialog_ = new cr.ui.dialogs.BaseDialog(dialogContainer);
this.learnMoreDialog_.showHtml(
title, html, undefined, this.onLearnMoreDialogClosed_.bind(this));
this.parentNode.disabled = true;
},
/**
* Clean up after the Learn more dialog is closed.
*/
onLearnMoreDialogClosed_: function() {
this.parentNode.disabled = false;
this.learnMoreDialog_ = undefined;
},
/**
* Retrieves the list of keyboard layouts available for the currently
* selected locale.
*/
getPublicSessionKeyboardLayouts_: function() {
var selectedLocale = this.querySelector('.language-select').value;
if (selectedLocale ==
this.querySelector('.keyboard-select').loadedLocale) {
// If the list of keyboard layouts was loaded for the currently selected
// locale, it is already up to date.
return;
}
chrome.send('getPublicSessionKeyboardLayouts',
[this.user.username, selectedLocale]);
},
/**
* Populates the keyboard layout "select" element with a list of layouts.
* @param {string} locale The locale to which this list of keyboard layouts
* applies
* @param {!Object} list List of available keyboard layouts
*/
populateKeyboardSelect: function(locale, list) {
if (locale != this.querySelector('.language-select').value) {
// The selected locale has changed and the list of keyboard layouts is
// not applicable. This method will be called again when a list of
// keyboard layouts applicable to the selected locale is retrieved.
return;
}
var keyboardSelect = this.querySelector('.keyboard-select');
keyboardSelect.loadedLocale = locale;
keyboardSelect.innerHTML = '';
for (var i = 0; i < list.length; ++i) {
var item = list[i];
keyboardSelect.appendChild(
new Option(item.title, item.value, item.selected, item.selected));
}
},
/**
* Populates the language "select" element with a list of locales.
* @param {!Object} locales The list of available locales
* @param {string} defaultLocale The locale to select by default
* @param {boolean} multipleRecommendedLocales Whether |locales| contains
* two or more recommended locales
*/
populateLanguageSelect: function(
locales, defaultLocale, multipleRecommendedLocales) {
var languageSelect = this.querySelector('.language-select');
// If the user manually selected a locale, do not change the selection.
// Otherwise, select the new |defaultLocale|.
var selected =
languageSelect.manuallyChanged ? languageSelect.value : defaultLocale;
languageSelect.innerHTML = '';
var group = languageSelect;
for (var i = 0; i < locales.length; ++i) {
var item = locales[i];
if (item.optionGroupName) {
group = document.createElement('optgroup');
group.label = item.optionGroupName;
languageSelect.appendChild(group);
} else {
group.appendChild(new Option(item.title,
item.value,
item.value == selected,
item.value == selected));
}
}
languageSelect.multipleRecommendedLocales = multipleRecommendedLocales;
// Retrieve a list of keyboard layouts applicable to the locale that is
// now selected.
this.getPublicSessionKeyboardLayouts_();
}
};
/**
* Creates a user pod to be used only in desktop chrome.
* @constructor
* @extends {UserPod}
*/
var DesktopUserPod = cr.ui.define(function() {
// Don't just instantiate a UserPod(), as this will call decorate() on the
// parent object, and add duplicate event listeners.
var node = $('user-pod-template').cloneNode(true);
node.removeAttribute('id');
return node;
});
DesktopUserPod.prototype = {
__proto__: UserPod.prototype,
/** @override */
initialize: function() {
if (this.user.needsSignin) {
if (this.user.hasLocalCreds) {
this.user.initialAuthType = AUTH_TYPE.OFFLINE_PASSWORD;
} else {
this.user.initialAuthType = AUTH_TYPE.ONLINE_SIGN_IN;
}
}
UserPod.prototype.initialize.call(this);
},
/** @override */
get mainInput() {
if (this.user.needsSignin)
return this.passwordElement;
else
return this.nameElement;
},
/** @override */
update: function() {
this.imageElement.src = this.user.userImage;
this.smallPodImageElement.src = this.user.userImage;
this.nameElement.textContent = this.user.displayName;
this.smallPodNameElement.textContent = this.user.displayName;
this.reauthNameHintElement.textContent = this.user.displayName;
var isLockedUser = this.user.needsSignin;
var isLegacySupervisedUser = this.user.legacySupervisedUser;
var isChildUser = this.user.childUser;
var isSyncedUser = this.user.emailAddress !== "";
var isProfileLoaded = this.user.isProfileLoaded;
this.classList.toggle('locked', isLockedUser);
this.classList.toggle('legacy-supervised', isLegacySupervisedUser);
this.classList.toggle('child', isChildUser);
this.classList.toggle('synced', isSyncedUser);
if (this.isAuthTypeUserClick)
this.passwordLabelElement.textContent = this.authValue;
this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
'passwordFieldAccessibleName', this.user_.emailAddress));
UserPod.prototype.updateActionBoxArea.call(this);
},
/** @override */
activate: function(e) {
if (!this.user.needsSignin) {
Oobe.launchUser(this.user.profilePath);
} else if (this.user.hasLocalCreds && !this.passwordElement.value) {
return false;
} else {
chrome.send('authenticatedLaunchUser',
[this.user.profilePath,
this.user.emailAddress,
this.passwordElement.value]);
}
this.passwordElement.value = '';
return true;
},
/** @override */
handleClickOnPod_: function(e) {
if (this.parentNode.disabled)
return;
if (this.getPodStyle() != UserPod.Style.LARGE) {
$('pod-row').switchMainPod(this);
return;
}
Oobe.clearErrors();
this.parentNode.lastFocusedPod_ = this;
// If this is a locked pod and there are local credentials, show the
// password field. Otherwise call activate() which will open up a browser
// window or show the reauth dialog, as needed.
if (!(this.user.needsSignin && this.user.hasLocalCreds) &&
!this.isActionBoxMenuActive) {
this.activate(e);
}
if (this.isAuthTypeUserClick)
chrome.send('attemptUnlock', [this.user.emailAddress]);
},
};
/**
* Creates a user pod that represents kiosk app.
* @constructor
* @extends {UserPod}
*/
var KioskAppPod = cr.ui.define(function() {
var node = UserPod();
return node;
});
KioskAppPod.prototype = {
__proto__: UserPod.prototype,
/** @override */
decorate: function() {
UserPod.prototype.decorate.call(this);
this.launchAppButtonElement.addEventListener('click',
this.activate.bind(this));
},
/** @override */
update: function() {
this.imageElement.src = this.user.iconUrl;
this.imageElement.alt = this.user.label;
this.imageElement.title = this.user.label;
this.smallPodImageElement.src = this.user.iconUrl;
this.smallPodImageElement.alt = this.user.label;
this.smallPodImageElement.title = this.user.label;
this.passwordEntryContainerElement.hidden = true;
this.launchAppButtonContainerElement.hidden = false;
this.nameElement.textContent = this.user.label;
this.smallPodNameElement.textContent = this.user.label;
this.reauthNameHintElement.textContent = this.user.label;
UserPod.prototype.updateActionBoxArea.call(this);
UserPod.prototype.customizeUserPodPerUserType.call(this);
},
/** @override */
get mainInput() {
return this.launchAppButtonElement;
},
/** @override */
focusInput: function() {
// Move tabIndex from the whole pod to the main input.
this.tabIndex = -1;
this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
this.mainInput.focus();
},
/** @override */
get forceOnlineSignin() {
return false;
},
/** @override */
activate: function(e) {
var diagnosticMode = e && e.ctrlKey;
this.launchApp_(this.user, diagnosticMode);
return true;
},
/** @override */
handleClickOnPod_: function(e) {
if (this.parentNode.disabled)
return;
if (this.getPodStyle() != UserPod.Style.LARGE) {
$('pod-row').switchMainPod(this);
return;
}
Oobe.clearErrors();
this.parentNode.lastFocusedPod_ = this;
this.activate(e);
},
/**
* Launch the app. If |diagnosticMode| is true, ask user to confirm.
* @param {Object} app App data.
* @param {boolean} diagnosticMode Whether to run the app in diagnostic
* mode.
*/
launchApp_: function(app, diagnosticMode) {
if (!diagnosticMode) {
chrome.send('launchKioskApp', [app.id, false]);
return;
}
var oobe = $('oobe');
if (!oobe.confirmDiagnosticMode_) {
oobe.confirmDiagnosticMode_ =
new cr.ui.dialogs.ConfirmDialog(document.body);
oobe.confirmDiagnosticMode_.setOkLabel(
loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
oobe.confirmDiagnosticMode_.setCancelLabel(
loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
}
oobe.confirmDiagnosticMode_.show(
loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
app.label),
function() {
chrome.send('launchKioskApp', [app.id, true]);
});
},
};
/**
* Creates a new pod row element.
* @constructor
* @extends {HTMLDivElement}
*/
var PodRow = cr.ui.define('podrow');
PodRow.prototype = {
__proto__: HTMLDivElement.prototype,
// Whether this user pod row is shown for the first time.
firstShown_: true,
// True if inside focusPod().
insideFocusPod_: false,
// Focused pod.
focusedPod_: undefined,
// Activated pod, i.e. the pod of current login attempt.
activatedPod_: undefined,
// Pod that was most recently focused, if any.
lastFocusedPod_: undefined,
// Pod that occupies the main spot.
mainPod_: undefined,
// Pods whose initial images haven't been loaded yet.
podsWithPendingImages_: [],
// Whether pod placement has been postponed.
podPlacementPostponed_: false,
// Standard user pod height/width.
userPodHeight_: 0,
userPodWidth_: 0,
// Array of apps that are shown in addition to other user pods.
apps_: [],
// True to show app pods along with user pods.
shouldShowApps_: true,
// Array of users that are shown (public/supervised/regular).
users_: [],
// If we're in tablet mode.
tabletModeEnabled_: false,
// If testing mode is enabled.
testingModeEnabled_: false,
// The color used by the scroll list when the user count exceeds
// LANDSCAPE_MODE_LIMIT or PORTRAIT_MODE_LIMIT.
overlayColors_: {maskColor: undefined, scrollColor: undefined},
// Whether we should add background behind user pods.
showPodBackground_: false,
/** @override */
decorate: function() {
// Event listeners that are installed for the time period during which
// the element is visible.
this.listeners_ = {
focus: [this.handleFocus_.bind(this), true /* useCapture */],
click: [this.handleClick_.bind(this), true],
mousemove: [this.handleMouseMove_.bind(this), false],
keydown: [this.handleKeyDown.bind(this), false]
};
var isDesktopUserManager = Oobe.getInstance().displayType ==
DISPLAY_TYPE.DESKTOP_USER_MANAGER;
var isNewDesktopUserManager = Oobe.getInstance().newDesktopUserManager;
this.userPodHeight_ = isDesktopUserManager ?
isNewDesktopUserManager ? MD_DESKTOP_POD_HEIGHT :
DESKTOP_POD_HEIGHT :
CROS_POD_HEIGHT;
this.userPodWidth_ = isDesktopUserManager ?
isNewDesktopUserManager ? MD_DESKTOP_POD_WIDTH :
DESKTOP_POD_WIDTH :
CROS_POD_WIDTH;
},
/**
* Returns all the pods in this pod row. Some pods may not be its direct
* children, but the caller doesn't have to know this.
* @type {NodeList}
*/
get pods() {
var powRowChildren = Array.prototype.slice.call(this.children);
var containerChildren =
Array.prototype.slice.call(this.smallPodsContainer.children);
return powRowChildren.concat(containerChildren);
},
/**
* Return true if user pod row has only single user pod in it, which should
* always be focused except desktop mode.
* @type {boolean}
*/
get alwaysFocusSinglePod() {
return Oobe.getInstance().displayType !=
DISPLAY_TYPE.DESKTOP_USER_MANAGER &&
this.pods.length == 1;
},
/**
* Returns pod with the given app id.
* @param {!string} app_id Application id to be matched.
* @return {Object} Pod with the given app id. null if pod hasn't been
* found.
*/
getPodWithAppId_: function(app_id) {
for (var i = 0, pod; pod = this.pods[i]; ++i) {
if (pod.user.isApp && pod.user.id == app_id)
return pod;
}
return null;
},
/**
* Returns pod with the given username (null if there is no such pod).
* @param {string} username Username to be matched.
* @return {Object} Pod with the given username. null if pod hasn't been
* found.
*/
getPodWithUsername_: function(username) {
for (var i = 0, pod; pod = this.pods[i]; ++i) {
if (pod.user.username == username)
return pod;
}
return null;
},
/**
* True if the the pod row is disabled (handles no user interaction).
* @type {boolean}
*/
disabled_: false,
get disabled() {
return this.disabled_;
},
set disabled(value) {
this.disabled_ = value;
this.pods.forEach(function(pod) {
pod.disabled = value;
});
},
/**
* Creates a user pod from given email.
* @param {!Object} user User info dictionary.
*/
createUserPod: function(user) {
var userPod;
if (user.isDesktopUser)
userPod = new DesktopUserPod({user: user});
else if (user.publicAccount)
userPod = new PublicAccountUserPod({user: user});
else if (user.isApp)
userPod = new KioskAppPod({user: user});
else
userPod = new UserPod({user: user});
userPod.hidden = false;
return userPod;
},
/**
* Add an existing user pod to this pod row.
* @param {!Object} user User info dictionary.
*/
addUserPod: function(user) {
var userPod = this.createUserPod(user);
this.appendChild(userPod);
userPod.initialize();
},
/**
* Performs visual changes on the user pod if there is an error.
* @param {boolean} visible Whether to show or hide the display.
*/
setFocusedPodErrorDisplay: function(visible) {
if (this.focusedPod_)
this.focusedPod_.showError = visible;
},
/**
* Shows or hides the pin keyboard for the current focused pod.
* @param {boolean} visible
*/
setFocusedPodPinVisibility: function(visible) {
if (this.focusedPod_)
this.focusedPod_.setPinVisibility(visible);
},
/**
* Runs app with a given id from the list of loaded apps.
* @param {!string} app_id of an app to run.
* @param {boolean=} opt_diagnosticMode Whether to run the app in
* diagnostic mode. Default is false.
*/
findAndRunAppForTesting: function(app_id, opt_diagnosticMode) {
var app = this.getPodWithAppId_(app_id);
if (app) {
var activationEvent = cr.doc.createEvent('MouseEvents');
var ctrlKey = opt_diagnosticMode;
activationEvent.initMouseEvent('click', true, true, null,
0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
app.dispatchEvent(activationEvent);
}
},
/**
* Enables or disables the pin keyboard for the given user. A disabled pin
* keyboard will never be displayed.
*
* If the user's pod is focused, then enabling the pin keyboard will display
* it; disabling the pin keyboard will hide it.
* @param {!string} username
* @param {boolean} enabled
*/
setPinEnabled: function(username, enabled) {
var pod = this.getPodWithUsername_(username);
if (!pod) {
console.error('Attempt to enable/disable pin keyboard of missing pod.');
return;
}
// Make sure to set |pinEnabled| before toggling visiblity to avoid
// validation errors.
pod.pinEnabled = enabled;
if (this.focusedPod_ == pod) {
if (enabled) {
ensurePinKeyboardLoaded(
this.setPinVisibility.bind(this, username, true));
} else {
this.setPinVisibility(username, false);
}
}
},
/**
* Shows or hides the pin keyboard from the pod with the given |username|.
* This is only a visibility change; the pin keyboard can be reshown.
*
* Use setPinEnabled if the pin keyboard should be disabled for the given
* user.
* @param {!user} username
* @param {boolean} visible
*/
setPinVisibility: function(username, visible) {
var pod = this.getPodWithUsername_(username);
if (!pod) {
console.error('Attempt to show/hide pin keyboard of missing pod.');
return;
}
if (visible && pod.pinEnabled === false) {
console.error('Attempt to show disabled pin keyboard');
return;
}
if (visible && this.focusedPod_ != pod) {
console.error('Attempt to show pin keyboard on non-focused pod');
return;
}
pod.setPinVisibility(visible);
},
/**
* Removes user pod from pod row.
* @param {!user} username
*/
removeUserPod: function(username) {
var podToRemove = this.getPodWithUsername_(username);
if (podToRemove == null) {
console.warn('Attempt to remove pod that does not exist');
return;
}
// Its parent is not necessarily this pod row.
podToRemove.parentNode.removeChild(podToRemove);
this.mainPod_ = null;
if (this.pods.length > 0) {
// placePods_() will select a new main pod and re-append pods
// to different parents if necessary.
this.placePods_();
this.maybePreselectPod();
}
},
/**
* Returns index of given pod or -1 if not found.
* @param {UserPod} pod Pod to look up.
* @private
*/
indexOf_: function(pod) {
for (var i = 0; i < this.pods.length; ++i) {
if (pod == this.pods[i])
return i;
}
return -1;
},
/**
* Populates pod row with given existing users and start init animation.
* @param {array} users Array of existing user emails.
*/
loadPods: function(users) {
this.users_ = users;
this.rebuildPods();
},
/**
* Rebuilds pod row using users_ and apps_ that were previously set or
* updated.
*/
rebuildPods: function() {
var emptyPodRow = this.pods.length == 0;
// Clear existing pods.
this.innerHTML = '';
this.focusedPod_ = undefined;
this.activatedPod_ = undefined;
this.lastFocusedPod_ = undefined;
this.mainPod_ = undefined;
this.smallPodsContainer.innerHTML = '';
this.topMask.innerHTML = '';
this.bottomMask.innerHTML = '';
// Switch off animation
Oobe.getInstance().toggleClass('flying-pods', false);
for (var i = 0; i < this.users_.length; ++i)
this.addUserPod(this.users_[i]);
for (var i = 0, pod; pod = this.pods[i]; ++i)
this.podsWithPendingImages_.push(pod);
// TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
if (this.shouldShowApps_) {
for (var i = 0; i < this.apps_.length; ++i)
this.addUserPod(this.apps_[i]);
}
// Make sure we eventually show the pod row, even if some image is stuck.
setTimeout(function() {
$('pod-row').classList.remove('images-loading');
this.smallPodsContainer.classList.remove('images-loading');
this.topMask.classList.remove('images-loading');
this.bottomMask.classList.remove('images-loading');
}.bind(this), POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
var isAccountPicker = $('login-header-bar').signinUIState ==
SIGNIN_UI_STATE.ACCOUNT_PICKER;
// Immediately recalculate pods layout only when current UI is account
// picker. Otherwise postpone it.
if (isAccountPicker) {
this.placePods_();
this.maybePreselectPod();
// Without timeout changes in pods positions will be animated even
// though it happened when 'flying-pods' class was disabled.
setTimeout(function() {
Oobe.getInstance().toggleClass('flying-pods', true);
}, 0);
} else {
this.podPlacementPostponed_ = true;
// Update [Cancel] button state.
if ($('login-header-bar').signinUIState ==
SIGNIN_UI_STATE.GAIA_SIGNIN &&
emptyPodRow &&
this.pods.length > 0) {
login.GaiaSigninScreen.updateControlsState();
}
}
},
/**
* Gets the container of small pods.
* @type {!HTMLDivElement}
*/
get smallPodsContainer() {
return document.querySelector('.small-pod-container');
},
/**
* Gets the gradient mask at the top.
* @type {!HTMLDivElement}
*/
get topMask() {
return document.querySelector('.small-pod-container-mask');
},
/**
* Gets the gradient mask at the bottom.
* @type {!HTMLDivElement}
*/
get bottomMask() {
return document.querySelector('.small-pod-container-mask.rotate');
},
/**
* Adds given apps to the pod row.
* @param {array} apps Array of apps.
*/
setApps: function(apps) {
this.apps_ = apps;
this.rebuildPods();
chrome.send('kioskAppsLoaded');
// Check whether there's a pending kiosk app error.
window.setTimeout(function() {
chrome.send('checkKioskAppLaunchError');
}, 500);
},
/**
* Sets whether should show app pods.
* @param {boolean} shouldShowApps Whether app pods should be shown.
*/
setShouldShowApps: function(shouldShowApps) {
if (this.shouldShowApps_ == shouldShowApps)
return;
this.shouldShowApps_ = shouldShowApps;
this.rebuildPods();
},
/**
* Shows a custom icon on a user pod besides the input field.
* @param {string} username Username of pod to add button
* @param {!{id: !string,
* hardlockOnClick: boolean,
* isTrialRun: boolean,
* ariaLabel: string | undefined,
* tooltip: ({text: string, autoshow: boolean} | undefined)}} icon
* The icon parameters.
*/
showUserPodCustomIcon: function(username, icon) {
var pod = this.getPodWithUsername_(username);
if (pod == null) {
console.error('Unable to show user pod button: user pod not found.');
return;
}
if (!icon.id && !icon.tooltip)
return;
if (icon.id)
pod.customIconElement.setIcon(icon.id);
if (icon.isTrialRun) {
pod.customIconElement.setInteractive(
this.onDidClickLockIconDuringTrialRun_.bind(this, username));
} else if (icon.hardlockOnClick) {
pod.customIconElement.setInteractive(
this.hardlockUserPod_.bind(this, username));
} else {
pod.customIconElement.setInteractive(null);
}
var ariaLabel = icon.ariaLabel || (icon.tooltip && icon.tooltip.text);
if (ariaLabel)
pod.customIconElement.setAriaLabel(ariaLabel);
else
console.warn('No ARIA label for user pod custom icon.');
pod.customIconElement.show();
// This has to be called after |show| in case the tooltip should be shown
// immediatelly.
pod.customIconElement.setTooltip(
icon.tooltip || {text: '', autoshow: false});
// Hide fingerprint icon when custom icon is shown.
this.setUserPodFingerprintIcon(username, FINGERPRINT_STATES.HIDDEN);
},
/**
* Hard-locks user pod for the user. If user pod is hard-locked, it can be
* only unlocked using password, and the authentication type cannot be
* changed.
* @param {!string} username The user's username.
* @private
*/
hardlockUserPod_: function(username) {
chrome.send('hardlockPod', [username]);
},
/**
* Records a metric indicating that the user clicked on the lock icon during
* the trial run for Easy Unlock.
* @param {!string} username The user's username.
* @private
*/
onDidClickLockIconDuringTrialRun_: function(username) {
chrome.send('recordClickOnLockIcon', [username]);
},
/**
* Hides the custom icon in the user pod added by showUserPodCustomIcon().
* @param {string} username Username of pod to remove button
*/
hideUserPodCustomIcon: function(username) {
var pod = this.getPodWithUsername_(username);
if (pod == null) {
console.error('Unable to hide user pod button: user pod not found.');
return;
}
// TODO(tengs): Allow option for a fading transition.
pod.customIconElement.hide();
// Show fingerprint icon if applicable.
this.setUserPodFingerprintIcon(username, FINGERPRINT_STATES.DEFAULT);
},
/**
* Set a fingerprint icon in the user pod of |username|.
* @param {string} username Username of the selected user
* @param {number} state Fingerprint unlock state
*/
setUserPodFingerprintIcon: function(username, state) {
var pod = this.getPodWithUsername_(username);
if (pod == null) {
console.error(
'Unable to set user pod fingerprint icon: user pod not found.');
return;
}
pod.fingerprintAuthenticated_ = false;
if (!pod.fingerprintIconElement)
return;
if (!pod.user.allowFingerprint || state == FINGERPRINT_STATES.HIDDEN ||
!pod.customIconElement.hidden) {
pod.fingerprintIconElement.hidden = true;
pod.submitButton.hidden = false;
return;
}
FINGERPRINT_STATES_MAPPING.forEach(function(icon) {
pod.fingerprintIconElement.classList.toggle(
icon.class, state == icon.state);
});
pod.fingerprintIconElement.hidden = false;
pod.submitButton.hidden = pod.passwordElement.value.length == 0;
this.updatePasswordField_(pod, state);
if (state == FINGERPRINT_STATES.DEFAULT)
return;
pod.fingerprintAuthenticated_ = true;
this.setActivatedPod(pod);
if (state == FINGERPRINT_STATES.FAILED) {
/** @const */ var RESET_ICON_TIMEOUT_MS = 500;
setTimeout(
this.resetIconAndPasswordField_.bind(this, pod),
RESET_ICON_TIMEOUT_MS);
}
},
/**
* Reset the fingerprint icon and password field.
* @param {UserPod} pod Pod to reset.
*/
resetIconAndPasswordField_: function(pod) {
if (!pod.fingerprintIconElement)
return;
this.setUserPodFingerprintIcon(
pod.user.username, FINGERPRINT_STATES.DEFAULT);
},
/**
* Remove the fingerprint icon in the user pod.
* @param {string} username Username of the selected user
*/
removeUserPodFingerprintIcon: function(username) {
var pod = this.getPodWithUsername_(username);
if (pod == null) {
console.error('No user pod found (when removing fingerprint icon).');
return;
}
this.resetIconAndPasswordField_(pod);
if (pod.fingerprintIconElement) {
pod.fingerprintIconElement.parentNode.removeChild(
pod.fingerprintIconElement);
}
pod.submitButton.hidden = false;
},
/**
* Updates the password field in the user pod.
* @param {UserPod} pod Pod to update.
* @param {number} state Fingerprint unlock state
*/
updatePasswordField_: function(pod, state) {
FINGERPRINT_STATES_MAPPING.forEach(function(item) {
pod.passwordElement.classList.toggle(item.class, state == item.state);
});
var placeholderStr = loadTimeData.getString(
pod.isPinShown() ? 'pinKeyboardPlaceholderPinPassword' :
'passwordHint');
if (state == FINGERPRINT_STATES.SIGNIN) {
placeholderStr = loadTimeData.getString('fingerprintSigningin');
} else if (state == FINGERPRINT_STATES.FAILED) {
placeholderStr = loadTimeData.getString('fingerprintSigninFailed');
}
pod.passwordElement.placeholder = placeholderStr;
},
/**
* Sets the authentication type used to authenticate the user.
* @param {string} username Username of selected user
* @param {number} authType Authentication type, must be one of the
* values listed in AUTH_TYPE enum.
* @param {string} value The initial value to use for authentication.
*/
setAuthType: function(username, authType, value) {
var pod = this.getPodWithUsername_(username);
if (pod == null) {
console.error('Unable to set auth type: user pod not found.');
return;
}
pod.setAuthType(authType, value);
},
/**
* Sets the state of tablet mode.
* @param {boolean} isTabletModeEnabled true if the mode is on.
*/
setTabletModeState: function(isTabletModeEnabled) {
this.tabletModeEnabled_ = isTabletModeEnabled;
this.pods.forEach(function(pod, index) {
pod.actionBoxAreaElement.classList.toggle(
'forced', isTabletModeEnabled);
if (pod.isPublicSessionPod) {
pod.querySelector('.button-container')
.classList.toggle('forced', isTabletModeEnabled);
}
});
},
/**
* Updates the display name shown on a public session pod.
* @param {string} userID The user ID of the public session
* @param {string} displayName The new display name
*/
setPublicSessionDisplayName: function(userID, displayName) {
var pod = this.getPodWithUsername_(userID);
if (pod != null)
pod.setDisplayName(displayName);
},
/**
* Updates the list of locales available for a public session.
* @param {string} userID The user ID of the public session
* @param {!Object} locales The list of available locales
* @param {string} defaultLocale The locale to select by default
* @param {boolean} multipleRecommendedLocales Whether |locales| contains
* two or more recommended locales
*/
setPublicSessionLocales: function(
userID, locales, defaultLocale, multipleRecommendedLocales) {
var pod = this.getPodWithUsername_(userID);
if (pod != null) {
pod.populateLanguageSelect(locales,
defaultLocale,
multipleRecommendedLocales);
}
},
/**
* Updates the list of available keyboard layouts for a public session pod.
* @param {string} userID The user ID of the public session
* @param {string} locale The locale to which this list of keyboard layouts
* applies
* @param {!Object} list List of available keyboard layouts
*/
setPublicSessionKeyboardLayouts: function(userID, locale, list) {
var pod = this.getPodWithUsername_(userID);
if (pod != null)
pod.populateKeyboardSelect(locale, list);
},
/**
* Called when window was resized. The two common use cases are changing
* screen orientation and showing the virtual keyboard.
*/
onWindowResize: function() {
var isAccountPicker =
$('login-header-bar').signinUIState == SIGNIN_UI_STATE.ACCOUNT_PICKER;
if (isAccountPicker) {
// Redo pod placement if account picker is the current screen.
this.placePods_();
} else {
// Postpone pod placement. |handleBeforeShow| will check this flag.
this.podPlacementPostponed_ = true;
}
},
/**
* Places pods onto their positions in pod grid matching the new design.
* @private
*/
placePods_: function() {
var pods = this.pods;
if (pods.length == 0) {
console.error('Attempt to place pods for an empty pod list.');
return;
}
// Appends all pods to their proper parents. Small pods have parent other
// than the pod row. The pods were all initialized with the pow row as a
// temporary parent, which is intended to ensure that all event listeners
// work properly. If the main pod already exists, it means we are in the
// process of resizing the window, then there is no need to change parents
// of any pod.
if (!this.mainPod_) {
this.mainPod_ = this.preselectedPod;
this.appendPodsToParents_();
}
this.handleBeforePodPlacement_();
if (this.isScreenShrinked_()) {
// When virtual keyboard is shown, the account picker should occupy
// all the remaining screen. Screen size was already updated to exclude
// the virtual keyboard.
this.parentNode.setPreferredSize(
this.screenSize.width, this.screenSize.height);
} else {
// Make sure not to block the header bar when virtual keyboard is absent.
this.parentNode.setPreferredSize(
Oobe.getInstance().clientAreaSize.width,
Oobe.getInstance().clientAreaSize.height);
}
if (pods.length == 1) {
this.placeSinglePod_();
} else if (pods.length == POD_ROW_LIMIT) {
this.placePodsOnPodRow_();
} else {
this.placePodsOnContainer_();
}
Oobe.getInstance().updateScreenSize(this.parentNode);
this.handleAfterPodPlacement_();
},
/**
* Appends pods to proper parents. Called each time before pod placement.
* @private
*/
appendPodsToParents_: function() {
var pods = this.pods;
// Pod count may have changed, so the placement method may change
// accordingly. Therefore, always remove all pods from their current
// parents first.
for (var pod of pods) {
pod.parentNode.removeChild(pod);
}
if (pods.length <= POD_ROW_LIMIT) {
for (var pod of pods) {
this.appendChild(pod);
}
} else {
// When the user count exceeds the limit (currently set to 2), only the
// main pod still has pow row as parent, all other pods should be
// appended to the container with scroll bar.
for (var pod of pods) {
if (pod == this.mainPod_)
this.appendChild(pod);
else
this.smallPodsContainer.appendChild(pod);
}
}
},
/**
* Makes the screen unscrollable and hides the empty area underneath when
* virtual keyboard is shown.
* @private
*/
hideEmptyArea_: function() {
// When virtual keyboard is shown, although the screen size is reduced
// properly, the size of #outer-container remains the same in order to
// make other screens (e.g. Add person) scrollable, but this shouldn't
// apply to account picker.
// This is a hacky solution: we can make #scroll-container hide the
// overflow area and manully position #inner-container.
// NOTE: The global states set here might need to be cleared in
// handleHide. Please update the code there when adding new stuff here.
var isScreenShrinked = this.isScreenShrinked_();
$('scroll-container')
.classList.toggle('disable-scroll', isScreenShrinked);
$('inner-container').classList.toggle('disable-scroll', isScreenShrinked);
$('inner-container').style.top = isScreenShrinked ?
cr.ui.toCssPx($('scroll-container').scrollTop) :
'unset';
},
/**
* Called when there is one user pod.
* @private
*/
placeSinglePod_: function() {
this.mainPod_.setPodStyle(UserPod.Style.LARGE);
this.centerPod_(this.mainPod_, CROS_POD_WIDTH, CROS_POD_HEIGHT);
},
/**
* Called when the number of pods is within the POD_ROW_LIMIT.
* @private
*/
placePodsOnPodRow_: function() {
// Both pods have large size and are placed adjacently.
var secondPod =
this.pods[0] == this.mainPod_ ? this.pods[1] : this.pods[0];
this.mainPod_.setPodStyle(UserPod.Style.LARGE);
secondPod.setPodStyle(UserPod.Style.LARGE);
var DOUBLE_PODS_PADDING = this.isPortraitMode_() ? 32 : 118;
var leftPadding =
(this.screenSize.width - (CROS_POD_WIDTH * 2 + DOUBLE_PODS_PADDING)) /
2;
// Start actual positioning.
this.mainPod_.left = leftPadding;
this.mainPod_.top = (this.screenSize.height - CROS_POD_HEIGHT) / 2;
secondPod.left = leftPadding + CROS_POD_WIDTH + DOUBLE_PODS_PADDING;
secondPod.top = (this.screenSize.height - CROS_POD_HEIGHT) / 2;
},
/**
* Called when the number of pods exceeds the POD_ROW_LIMIT.
* @private
*/
placePodsOnContainer_: function() {
this.smallPodsContainer.hidden = false;
var pods = this.pods;
if ((pods.length > LANDSCAPE_MODE_LIMIT && !this.isPortraitMode_()) ||
(pods.length > PORTRAIT_MODE_LIMIT && this.isPortraitMode_())) {
// If the pod count exceeds limits, they should be in extra small size
// and the container will become scrollable.
this.placePodsOnScrollableContainer_();
return;
}
this.mainPod_.setPodStyle(UserPod.Style.LARGE);
for (var pod of pods) {
if (pod != this.mainPod_) {
// All pods except the main one must be set to the small style.
pod.setPodStyle(UserPod.Style.SMALL);
}
}
// The size of the smallPodsContainer must be updated to avoid overflow.
this.smallPodsContainer.style.height =
cr.ui.toCssPx(this.screenSize.height);
this.smallPodsContainer.style.width = cr.ui.toCssPx(CROS_SMALL_POD_WIDTH);
var LEFT_PADDING = this.isPortraitMode_() ? 0 : 98;
var MIDDLE_PADDING = this.isPortraitMode_() ? 84 : 220;
var contentsWidth = LEFT_PADDING +
CROS_POD_WIDTH + MIDDLE_PADDING + CROS_SMALL_POD_WIDTH;
var blankWidth = this.screenSize.width - contentsWidth;
var actualLeftPadding = LEFT_PADDING;
actualLeftPadding +=
this.isPortraitMode_() ? blankWidth * 2 / 3 : blankWidth / 2;
var SMALL_POD_PADDING = 54;
var actualSmallPodPadding = SMALL_POD_PADDING;
var smallPodsTotalHeight = (pods.length - 1) * CROS_SMALL_POD_HEIGHT +
(pods.length - 2) * actualSmallPodPadding;
// SCROLL_TOP_PADDING denotes the smallest top padding we can tolerate
// before allowing the container to overflow and show the scroll bar.
var SCROLL_TOP_PADDING = this.isPortraitMode_() ? 66 : 72;
if (smallPodsTotalHeight + SCROLL_TOP_PADDING * 2 >
this.screenSize.height) {
// In case the contents overflow for any reason (it shouldn't if the
// pod count is within limits), fall to the scrollable container case.
// But before that we'll try a smaller top padding and recalculate the
// total height if virtual keyboard is shown.
if (this.isScreenShrinked_()) {
actualSmallPodPadding = 32;
smallPodsTotalHeight = (pods.length - 1) * CROS_SMALL_POD_HEIGHT +
(pods.length - 2) * actualSmallPodPadding;
}
// If virtual keyboard is not shown, or the updated total height still
// exceeds screen height, fall to the scrollable container case.
if (smallPodsTotalHeight + SCROLL_TOP_PADDING * 2 >
this.screenSize.height) {
this.placePodsOnScrollableContainer_();
return;
}
}
// Start positioning of the main pod and the smallPodsContainer.
this.mainPod_.left = actualLeftPadding;
this.mainPod_.top = (this.screenSize.height - CROS_POD_HEIGHT) / 2;
this.smallPodsContainer.style.left =
cr.ui.toCssPx(actualLeftPadding + CROS_POD_WIDTH + MIDDLE_PADDING);
this.smallPodsContainer.style.top = cr.ui.toCssPx(0);
// Start positioning of the small pods inside the smallPodsContainer.
var smallPodsTopPadding =
(this.screenSize.height - smallPodsTotalHeight) / 2;
for (var pod of pods) {
if (pod == this.mainPod_) {
continue;
}
pod.left = 0;
pod.top = smallPodsTopPadding;
smallPodsTopPadding += CROS_SMALL_POD_HEIGHT + actualSmallPodPadding;
}
},
/**
* Called when the LANDSCAPE_MODE_LIMIT or PORTRAIT_MODE_LIMIT is exceeded
* and the scrollable container is shown.
* @private
*/
placePodsOnScrollableContainer_: function() {
this.smallPodsContainer.hidden = false;
// Add a dark overlay.
this.smallPodsContainer.classList.add('scroll');
if (this.overlayColors_.scrollColor) {
this.smallPodsContainer.style.backgroundColor =
this.overlayColors_.scrollColor;
}
var pods = this.pods;
this.mainPod_.setPodStyle(UserPod.Style.LARGE);
for (var pod of pods) {
if (pod != this.mainPod_) {
// All pods except the main one must be set to the extra small style.
pod.setPodStyle(UserPod.Style.EXTRA_SMALL);
}
}
var SCROLL_LEFT_PADDING = this.isPortraitMode_() ? 46 : 72;
var SCROLL_RIGHT_PADDING = this.isPortraitMode_() ? 12 : 72;
// The offsetWidth of the smallPodsContainer.
var scrollAreaWidth = SCROLL_LEFT_PADDING + CROS_EXTRA_SMALL_POD_WIDTH +
SCROLL_RIGHT_PADDING;
var mainPodPadding = (this.screenSize.width -
scrollAreaWidth - CROS_POD_WIDTH) / 2;
var SCROLL_TOP_PADDING = this.isPortraitMode_() ? 66 : 72;
var EXTRA_SMALL_POD_PADDING = 32;
// Start positioning of the main pod and the smallPodsContainer.
this.mainPod_.left = mainPodPadding;
this.mainPod_.top = (this.screenSize.height - CROS_POD_HEIGHT) / 2;
this.smallPodsContainer.style.left =
cr.ui.toCssPx(mainPodPadding * 2 + CROS_POD_WIDTH);
this.smallPodsContainer.style.top = cr.ui.toCssPx(0);
// Precalculate the total height of the scrollable container and check if
// it indeed exceeds the screen height.
var scrollHeight = 0;
for (var pod of pods) {
if (pod == this.mainPod_) {
continue;
}
scrollHeight += CROS_EXTRA_SMALL_POD_HEIGHT + EXTRA_SMALL_POD_PADDING;
}
scrollHeight -= EXTRA_SMALL_POD_PADDING;
// The smallPodsContainer should occupy the full screen vertically.
this.smallPodsContainer.style.height =
cr.ui.toCssPx(this.screenSize.height);
this.smallPodsContainer.style.width = cr.ui.toCssPx(
SCROLL_LEFT_PADDING + CROS_EXTRA_SMALL_POD_WIDTH +
SCROLL_RIGHT_PADDING);
// SCROLL_TOP_PADDING denotes the smallest top padding we can tolerate
// before allowing the container to overflow and show the scroll bar.
var actualTopPadding = SCROLL_TOP_PADDING;
if ((this.screenSize.height - scrollHeight) / 2 > actualTopPadding) {
// Edge case: the total height of the scrollable container does not
// exceed the screen height (minus the neceesary padding), so the
// scroll bar will not appear.
// In this case, we still want to keep the extra small pod size and
// the overlay, but the top and bottom padding should be adjusted
// to ensure a symmetric layout.
actualTopPadding = (this.screenSize.height - scrollHeight) / 2;
} else if (!this.isScreenShrinked_()) {
// The scroll bar will definitely be shown if we reach here. A gradient
// mask is applied to avoid blocking the header bar if the virtual
// keyboard is not shown. When the keyboard is shown, there's no need
// to add the mask and the original top padding value should be kept.
actualTopPadding = SCROLL_MASK_HEIGHT;
this.showScrollMask_();
}
// Start positioning of the small pods inside the smallPodsContainer.
var topPadding = actualTopPadding;
var lastPod = undefined;
for (var pod of pods) {
if (pod == this.mainPod_) {
continue;
}
pod.left = SCROLL_LEFT_PADDING;
pod.top = topPadding;
topPadding += CROS_EXTRA_SMALL_POD_HEIGHT + EXTRA_SMALL_POD_PADDING;
lastPod = pod;
}
// Make sure the last pod has a proper bottom padding for a symmetric
// layout.
lastPod.style.paddingBottom = cr.ui.toCssPx(actualTopPadding);
},
/**
* Called each time before pod placement to ensure we start with the
* initial state, which is ready to place only one user pod. The styles
* of elements necessary for other placement methods must be set
* explicitly each time.
* @private
*/
handleBeforePodPlacement_: function() {
this.smallPodsContainer.hidden = true;
this.topMask.hidden = true;
this.bottomMask.hidden = true;
this.smallPodsContainer.classList.remove('scroll');
if (this.overlayColors_.scrollColor)
this.smallPodsContainer.style.backgroundColor = 'unset';
var pods = this.pods;
for (var pod of pods) {
// There is a chance that one of the pods has a bottom padding, so
// reset all of them to be safe. This is because if the pod was at
// the last position in the scrollable container, a bottom padding
// was added to ensure a symmetric layout.
pod.style.paddingBottom = cr.ui.toCssPx(0);
}
this.hideEmptyArea_();
// Clear error bubbles whenever pod placement is happening, i.e., after
// orientation change, showing or hiding virtual keyboard, and user
// removal.
Oobe.clearErrors();
},
/**
* Checks if the screen is in portrait mode.
* @return {boolean} True if in portrait mode.
*/
isPortraitMode_: function() {
return this.screenSize.width < this.screenSize.height;
},
/**
* Checks if the screen is shrinked, i.e., when showing virtual keyboard.
* We used to check Oobe.getInstance().virtualKeyboardShown directly
* but there were occasional bugs because that value may not be updated yet
* during pod placement.
* @return {boolean} True if the screen is shrinked.
*/
isScreenShrinked_: function() {
return this.screenSize.height <= Oobe.getInstance().clientAreaSize.height;
},
/**
* Called when scroll bar is shown and we need a mask for the header bar.
* @private
*/
showScrollMask_: function() {
this.topMask.hidden = false;
this.topMask.style.left = this.smallPodsContainer.style.left;
this.topMask.style.width = this.smallPodsContainer.style.width;
this.bottomMask.hidden = false;
this.bottomMask.style.left = this.smallPodsContainer.style.left;
this.bottomMask.style.width = this.smallPodsContainer.style.width;
// The bottom mask should overlap with the header bar, and its z-index
// is chosen to ensure it does not block users from using the header bar.
this.bottomMask.style.top =
cr.ui.toCssPx(this.screenSize.height - SCROLL_MASK_HEIGHT);
if (this.overlayColors_.maskColor) {
var maskGradient = this.getMaskGradient_(this.overlayColors_.maskColor);
this.topMask.style.background = maskGradient;
this.bottomMask.style.background = maskGradient;
}
},
/**
* Called after pod placement and before showing the pod row. Updates
* elements whose style may depend on the pod placement outcome.
* @private
*/
handleAfterPodPlacement_: function() {
var pods = this.pods;
for (var pod of pods) {
if (pod.getPodStyle() != UserPod.Style.LARGE)
continue;
// Make sure that user name on each large pod is centered and extra
// long names don't exceed maximum pod width.
var nameArea = pod.querySelector('.name-container');
var leftMargin = (CROS_POD_WIDTH - pod.nameElement.offsetWidth) / 2;
if (leftMargin > 0) {
nameArea.style.left = cr.ui.toCssPx(leftMargin);
nameArea.style.right = 'auto';
} else {
pod.nameElement.style.width = cr.ui.toCssPx(CROS_POD_WIDTH);
nameArea.style.left = cr.ui.toCssPx(0);
nameArea.style.right = 'auto';
// For public session pods whose names are cut off, add a banner
// which shows the full name upon hovering.
if (pod.isPublicSessionPod && !pod.querySelector('.full-name')) {
var fullNameContainer = document.createElement('div');
fullNameContainer.classList.add('full-name');
fullNameContainer.textContent = pod.nameElement.textContent;
nameArea.appendChild(fullNameContainer);
}
}
// Update info container area for public session pods.
if (pod.isPublicSessionPod) {
var infoElement = pod.querySelector('.info');
var infoIcon = pod.querySelector('.learn-more');
var totalWidth = PUBLIC_SESSION_ICON_WIDTH + infoElement.offsetWidth;
var infoLeftMargin = (CROS_POD_WIDTH - totalWidth) / 2;
if (infoLeftMargin > 0) {
infoIcon.style.left = cr.ui.toCssPx(infoLeftMargin);
infoIcon.style.right = 'auto';
infoElement.style.left =
cr.ui.toCssPx(infoLeftMargin + PUBLIC_SESSION_ICON_WIDTH);
infoElement.style.right = 'auto';
} else {
infoIcon.style.left = cr.ui.toCssPx(0);
infoIcon.style.right = 'auto';
infoElement.style.left = cr.ui.toCssPx(PUBLIC_SESSION_ICON_WIDTH);
infoElement.style.right = 'auto';
infoElement.style.width = cr.ui.toCssPx(
CROS_POD_WIDTH - PUBLIC_SESSION_ICON_WIDTH -
infoElement.style.paddingLeft);
}
// If the public session pod is already expanded, make sure it's
// still centered.
if (pod.expanded)
this.centerPod_(pod, PUBLIC_EXPANDED_WIDTH, PUBLIC_EXPANDED_HEIGHT);
}
// Update action box menu position to ensure it doesn't overlap with
// elements outside the pod.
var actionBoxMenu = pod.querySelector('.action-box-menu');
var actionBoxButton = pod.querySelector('.action-box-button');
var MENU_TOP_PADDING = 7;
if (this.isPortraitMode_() && pods.length > 1) {
// Confine the menu inside the pod when it may overlap with outside
// elements.
actionBoxMenu.style.left = 'auto';
actionBoxMenu.style.right = cr.ui.toCssPx(0);
actionBoxMenu.style.top =
cr.ui.toCssPx(actionBoxButton.offsetHeight + MENU_TOP_PADDING);
} else if (!this.isPortraitMode_() && this.isScreenShrinked_()) {
// Prevent the virtual keyboard from blocking the remove user button.
actionBoxMenu.style.left = cr.ui.toCssPx(
pod.nameElement.offsetWidth + actionBoxButton.offsetWidth);
actionBoxMenu.style.right = 'auto';
actionBoxMenu.style.top = cr.ui.toCssPx(0);
} else {
actionBoxMenu.style.left = cr.ui.toCssPx(
pod.nameElement.offsetWidth + actionBoxButton.style.marginLeft);
actionBoxMenu.style.right = 'auto';
actionBoxMenu.style.top =
cr.ui.toCssPx(actionBoxButton.offsetHeight + MENU_TOP_PADDING);
}
// Update password container width based on the visibility of the
// custom icon container.
pod.passwordEntryContainerElement.classList.toggle(
'custom-icon-shown', !pod.customIconElement.hidden);
// Add ripple animation.
var actionBoxRippleEffect =
pod.querySelector('.action-box-button.ripple-circle');
actionBoxRippleEffect.style.left = cr.ui.toCssPx(
pod.nameElement.offsetWidth + actionBoxButton.style.marginLeft);
actionBoxRippleEffect.style.right = 'auto';
actionBoxRippleEffect.style.top =
cr.ui.toCssPx(actionBoxButton.style.marginTop);
// Adjust the vertical position of the pod if pin keyboard is shown.
if (pod.isPinShown() && !this.isScreenShrinked_())
pod.top = (this.screenSize.height - CROS_POD_HEIGHT_WITH_PIN) / 2;
// In the end, switch direction of the above elements if right-to-left
// language is used.
if (isRTL()) {
switchDirection(nameArea);
switchDirection(actionBoxMenu);
switchDirection(actionBoxRippleEffect);
if (pod.isPublicSessionPod) {
switchDirection(pod.querySelector('.info'));
switchDirection(pod.querySelector('.learn-more'));
}
}
}
this.updateSigninBannerPosition_();
this.togglePodBackground(this.showPodBackground_);
},
/**
* Updates the sign-in banner position if it's shown. Called each time
* after message update or pod placement, because the position of the
* banner dynamically depends on the pod positions.
* @private
*/
updateSigninBannerPosition_: function() {
var bannerContainer = $('signin-banner-container1');
if (bannerContainer.hidden)
return;
bannerContainer.style.top = cr.ui.toCssPx(this.mainPod_.top / 2);
if (this.pods.length <= POD_ROW_LIMIT) {
bannerContainer.style.left = cr.ui.toCssPx(
(this.screenSize.width - bannerContainer.offsetWidth) / 2);
} else {
var leftPadding = this.mainPod_.left -
(bannerContainer.offsetWidth - CROS_POD_WIDTH) / 2;
bannerContainer.style.left = cr.ui.toCssPx(Math.max(leftPadding, 0));
}
},
/**
* Handles required UI changes when a public session pod toggles the
* expanded state.
* @param {UserPod} pod Public session pod.
* @param {boolean} expanded Whether the pod is expanded or not.
*/
handlePublicPodExpansion: function(pod, expanded) {
// Hide all other elements in the account picker if the pod is expanded.
this.parentNode.classList.toggle('public-account-expanded', expanded);
if (expanded) {
this.centerPod_(pod, PUBLIC_EXPANDED_WIDTH, PUBLIC_EXPANDED_HEIGHT);
} else {
// Return the pod to its last position.
pod.left = pod.lastPosition.left;
pod.top = pod.lastPosition.top;
// Pod placement has already finished by this point, but we still need
// to make sure that the styles of all the elements in the pod row are
// updated before being shown.
this.handleAfterPodPlacement_();
}
},
/**
* Called when a pod needs to be centered.
* @param {UserPod} pod Pod to be centered.
* @param {number} podWidth The pod width.
* @param {number} podHeight The pod height.
* @private
*/
centerPod_: function(pod, podWidth, podHeight) {
// The original position of a public session pod is recorded in case of
// future need.
if (pod.isPublicSessionPod)
pod.lastPosition = {left: pod.left, top: pod.top};
// Avoid using offsetWidth and offsetHeight in case the pod is not fully
// loaded yet.
pod.left = (this.screenSize.width - podWidth) / 2;
pod.top = (this.screenSize.height - podHeight) / 2;
},
/**
* Toggles the animation for switching between main pod and small pod.
* @param {UserPod} pod Pod that needs to toggle the animation.
* @param {boolean} enabled Whether the switch animation is needed.
* @private
*/
toggleSwitchAnimation_: function(pod, enabled) {
pod.imageElement.classList.toggle('switch-image-animation', enabled);
pod.animatedImageElement.classList.toggle(
'switch-image-animation', enabled);
pod.smallPodImageElement.classList.toggle(
'switch-image-animation', enabled);
pod.smallPodAnimatedImageElement.classList.toggle(
'switch-image-animation', enabled);
},
/**
* Called when a small or extra small pod is clicked to trigger the switch
* with the main pod.
* @param {UserPod} pod Pod to be switched with the main pod.
*/
switchMainPod: function(pod) {
if (this.disabled) {
console.error('Cannot switch main pod while sign-in UI is disabled.');
return;
}
if (!this.mainPod_) {
console.error('Attempt to switch a non-existing main pod.');
return;
}
// Find the index of the small pod.
var insert = 0;
var children = pod.parentNode.children;
while (insert < children.length && children[insert] != pod)
insert++;
if (insert >= children.length) {
console.error('Attempt to switch a non-existing small pod.');
return;
}
// Switch style of the two pods.
this.mainPod_.setPodStyle(pod.getPodStyle());
pod.setPodStyle(UserPod.Style.LARGE);
// Add switch animation.
this.toggleSwitchAnimation_(this.mainPod_, true);
this.toggleSwitchAnimation_(pod, true);
setTimeout(function() {
var pods = this.pods;
for (var pod of pods)
this.toggleSwitchAnimation_(pod, false);
}.bind(this), POD_SWITCH_ANIMATION_DURATION_MS);
// Switch parent and position of the two pods.
var left = pod.left;
var top = pod.top;
// Edge case: paddingBottom should be switched too because there's a
// chance that the small pod was at the end of the scrollable container
// and had a non-zero paddingBottom.
var paddingBottom = pod.style.paddingBottom;
var parent = pod.parentNode;
parent.removeChild(pod);
this.appendChild(pod);
pod.left = this.mainPod_.left;
pod.top = this.mainPod_.top;
pod.style.paddingBottom = cr.ui.toCssPx(0);
this.removeChild(this.mainPod_);
// It must have the same index with the original small pod, instead
// of being appended as the last child, in order to maintain the tab
// order.
parent.insertBefore(this.mainPod_, children[insert]);
this.mainPod_.left = left;
this.mainPod_.top = top;
this.mainPod_.style.paddingBottom = paddingBottom;
this.mainPod_ = pod;
// The new main pod should already be focused but we need a focus update
// in order to focus on the input box.
this.focusPod(this.mainPod_, true /* force */);
this.handleAfterPodPlacement_();
},
/**
* Returns dimensions of screen including the header bar.
* @type {Object}
*/
get screenSize() {
var container = $('scroll-container');
return {width: container.offsetWidth, height: container.offsetHeight};
},
/**
* Displays a banner containing |message|. If the banner is already present
* this function updates the message in the banner.
* @param {string} message Text to be displayed or empty to hide the banner.
*/
showBannerMessage: function(message) {
var banner = $('signin-banner');
banner.textContent = message;
banner.classList.toggle('message-set', !!message);
$('signin-banner-container1').hidden = banner.textContent.length == 0;
this.updateSigninBannerPosition_();
},
/**
* Sets login screen overlay colors based on colors extracted from the
* wallpaper.
* @param {string} maskColor Color for the gradient mask.
* @param {string} scrollColor Color for the small pods container.
*/
setOverlayColors: function(maskColor, scrollColor) {
if (this.smallPodsContainer.classList.contains('scroll'))
this.smallPodsContainer.style.backgroundColor = scrollColor;
if (!this.topMask.hidden) {
var maskGradient = this.getMaskGradient_(maskColor);
this.topMask.style.background = maskGradient;
this.bottomMask.style.background = maskGradient;
}
// Save the colors because the scrollable container and the masks may
// become visible later.
this.overlayColors_.maskColor = maskColor;
this.overlayColors_.scrollColor = scrollColor;
},
/**
* Helper function to create a style string for the scroll masks.
* @param {string} maskColor Color for the gradient mask.
* @return {string} A CSS style.
*/
getMaskGradient_: function(maskColor) {
return 'linear-gradient(' + maskColor + ', transparent)';
},
/**
* Toggles the background behind user pods.
* @param {boolean} showPodBackground Whether to add background behind user
* pods.
*/
togglePodBackground: function(showPodBackground) {
this.showPodBackground_ = showPodBackground;
var pods = this.pods;
for (var pod of pods)
pod.classList.toggle('show-pod-background', showPodBackground);
$('login-header-bar')
.classList.toggle('translucent-background', showPodBackground);
var isShowingScrollList =
this.smallPodsContainer.classList.contains('scroll');
if (isShowingScrollList) {
if (showPodBackground) {
// The scroll list should use a fixed color to make sure the pods are
// legible.
this.smallPodsContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.9)';
} else if (this.overlayColors_.scrollColor) {
// Change the background back to the color extracted from wallpaper.
this.smallPodsContainer.style.backgroundColor =
this.overlayColors_.scrollColor;
}
}
// Edge case: when we add pod background, we also need to add extra
// padding to the pods if they are not placed on top of the scroll list.
// The padding may result in overflow, so we allow showing overflow here.
// An alternative is to adjust the size of the small pods container, but
// we want to avoid changing the pod placement for this edge case.
this.smallPodsContainer.classList.toggle(
'show-overflow', showPodBackground && !isShowingScrollList);
},
/**
* Whether the pod is currently focused.
* @param {UserPod} pod Pod to check for focus.
* @return {boolean} Pod focus status.
*/
isFocused: function(pod) {
return this.focusedPod_ == pod;
},
/**
* Focuses a given user pod or clear focus when given null.
* @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
* @param {boolean=} opt_force If true, forces focus update even when
* podToFocus is already focused.
* @param {boolean=} opt_skipInputFocus If true, don't focus on the input
* box of user pod.
*/
focusPod: function(podToFocus, opt_force, opt_skipInputFocus) {
if (this.isFocused(podToFocus) && !opt_force) {
// Calling focusPod w/o podToFocus means reset.
if (!podToFocus)
Oobe.clearErrors();
return;
}
// Make sure there's only one focusPod operation happening at a time.
if (this.insideFocusPod_) {
return;
}
this.insideFocusPod_ = true;
for (var i = 0, pod; pod = this.pods[i]; ++i) {
if (!this.alwaysFocusSinglePod) {
pod.isActionBoxMenuActive = false;
}
if (pod != podToFocus) {
pod.isActionBoxMenuHovered = false;
pod.classList.remove('focused');
pod.setPinVisibility(false);
this.setUserPodFingerprintIcon(
pod.user.username, FINGERPRINT_STATES.HIDDEN);
// On Desktop, the faded style is not set correctly, so we should
// manually fade out non-focused pods if there is a focused pod.
if (pod.user.isDesktopUser && podToFocus)
pod.classList.add('faded');
else
pod.classList.remove('faded');
pod.reset(false);
}
}
// Clear any error messages for previous pod.
if (!this.isFocused(podToFocus))
Oobe.clearErrors();
this.focusedPod_ = podToFocus;
if (podToFocus) {
// Only show the keyboard if it is fully loaded.
if (podToFocus.isPinReady())
podToFocus.setPinVisibility(true);
podToFocus.classList.remove('faded');
podToFocus.classList.add('focused');
if (!podToFocus.multiProfilesPolicyApplied) {
podToFocus.classList.toggle('signing-in', false);
if (!opt_skipInputFocus)
podToFocus.focusInput();
} else {
podToFocus.userTypeBubbleElement.classList.add('bubble-shown');
// Note it is not necessary to skip this focus request when
// |opt_skipInputFocus| is true. When |multiProfilesPolicyApplied|
// is false, it doesn't focus on the password input box by default.
podToFocus.focus();
}
if (!podToFocus.user.isApp) {
// Only updates wallpaper when the focused pod is in large style.
chrome.send('focusPod', [
podToFocus.user.username,
podToFocus.getPodStyle() == UserPod.Style.LARGE
]);
}
this.firstShown_ = false;
this.lastFocusedPod_ = podToFocus;
this.setUserPodFingerprintIcon(
podToFocus.user.username, FINGERPRINT_STATES.DEFAULT);
} else {
chrome.send('noPodFocused');
}
this.insideFocusPod_ = false;
},
/**
* Returns the currently activated pod.
* @type {UserPod}
*/
get activatedPod() {
return this.activatedPod_;
},
/**
* Sets currently activated pod.
* @param {UserPod} pod Pod to check for focus.
* @param {Event} e Event object.
*/
setActivatedPod: function(pod, e) {
if (this.disabled) {
console.error('Cannot activate pod while sign-in UI is disabled.');
return;
}
// If testing mode is enabled and a positive integer was entered, abort
// the activation process and start testing mode.
if (pod && this.testingModeEnabled_) {
var userCount = pod.passwordElement.value;
if (parseInt(userCount) == userCount && userCount > 0) {
this.showDummyUsersForTesting(userCount);
return;
}
}
if (pod && pod.activate(e))
this.activatedPod_ = pod;
},
/**
* Used for testing only. Create the specified number of dummy users and
* conveniently test the behaviors under different number of pods.
* @param {number} count The number of users we want to test for.
*/
showDummyUsersForTesting: function(count) {
if (!this.testingModeEnabled_) {
console.error(
'Attempt to create dummy users when testing mode is disabled.');
return;
}
var pods = this.pods;
for (var pod of pods)
pod.parentNode.removeChild(pod);
var sampleUser = this.users_[0];
var users = [];
for (var i = 0; i < count; i++)
users.push(sampleUser);
this.loadPods(users);
},
/**
* The pod of the signed-in user, if any; null otherwise.
* @type {?UserPod}
*/
get lockedPod() {
for (var i = 0, pod; pod = this.pods[i]; ++i) {
if (pod.user.signedIn)
return pod;
}
return null;
},
/**
* The pod that is preselected on user pod row show.
* @type {?UserPod}
*/
get preselectedPod() {
var isDesktopUserManager = Oobe.getInstance().displayType ==
DISPLAY_TYPE.DESKTOP_USER_MANAGER;
if (isDesktopUserManager) {
// On desktop, don't pre-select a pod if it's the only one.
if (this.pods.length == 1)
return null;
// The desktop User Manager can send an URI encoded profile path in the
// url hash, that indicates a pod that should be initially focused.
var focusedProfilePath =
decodeURIComponent(window.location.hash.substr(1));
for (var i = 0, pod; pod = this.pods[i]; ++i) {
if (focusedProfilePath === pod.user.profilePath)
return pod;
}
return null;
}
var lockedPod = this.lockedPod;
if (lockedPod)
return lockedPod;
for (i = 0; pod = this.pods[i]; ++i) {
if (!pod.multiProfilesPolicyApplied)
return pod;
}
return this.pods[0];
},
/**
* Resets input UI.
* @param {boolean} takeFocus True to take focus.
*/
reset: function(takeFocus) {
this.disabled = false;
if (this.activatedPod_)
this.activatedPod_.reset(takeFocus);
},
/**
* Restores input focus to current selected pod, if there is any.
*/
refocusCurrentPod: function() {
if (this.focusedPod_ && !this.focusedPod_.multiProfilesPolicyApplied) {
this.focusedPod_.focusInput();
}
},
/**
* Clears focused pod password field.
*/
clearFocusedPod: function() {
if (!this.disabled && this.focusedPod_)
this.focusedPod_.reset(true);
},
/**
* Shows signin UI.
* @param {string} email Email for signin UI.
*/
showSigninUI: function(email) {
// Clear any error messages that might still be around.
Oobe.clearErrors();
this.disabled = true;
this.lastFocusedPod_ = this.getPodWithUsername_(email);
Oobe.showSigninUI(email);
},
/**
* Updates current image of a user.
* @param {string} username User for which to update the image.
*/
updateUserImage: function(username) {
var pod = this.getPodWithUsername_(username);
if (pod)
pod.updateUserImage();
},
/**
* Handler of click event.
* @param {Event} e Click Event object.
* @private
*/
handleClick_: function(e) {
if (this.disabled)
return;
// Clear all menus if the click is outside pod menu and its
// button area.
if (!findAncestorByClass(e.target, 'action-box-menu') &&
!findAncestorByClass(e.target, 'action-box-area')) {
for (var i = 0, pod; pod = this.pods[i]; ++i)
pod.isActionBoxMenuActive = false;
}
// Clears focus if not clicked on a pod and if there's more than one pod.
var pod = findAncestorByClass(e.target, 'pod');
if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) {
this.focusPod();
}
if (pod && pod.getPodStyle() == UserPod.Style.LARGE)
pod.isActionBoxMenuHovered = true;
// Return focus back to single pod.
if (this.alwaysFocusSinglePod && !pod) {
if ($('login-header-bar').contains(e.target))
return;
this.focusPod(this.focusedPod_, true /* force */);
this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
this.focusedPod_.isActionBoxMenuHovered = false;
// If the click is outside the public session pod, still focus on it
// but do not expand it any more.
if (this.focusedPod_.isPublicSessionPod)
this.focusedPod_.expanded = false;
}
},
/**
* Handler of mouse move event.
* @param {Event} e Click Event object.
* @private
*/
handleMouseMove_: function(e) {
if (this.disabled)
return;
if (e.movementX == 0 && e.movementY == 0)
return;
// Defocus (thus hide) action box, if it is focused on a user pod
// and the pointer is not hovering over it.
var pod = findAncestorByClass(e.target, 'pod');
if (document.activeElement &&
document.activeElement.parentNode != pod &&
document.activeElement.classList.contains('action-box-area')) {
document.activeElement.parentNode.focus();
}
if (pod)
pod.isActionBoxMenuHovered = true;
// Hide action boxes on other user pods.
for (var i = 0, p; p = this.pods[i]; ++i)
if (p != pod && !p.isActionBoxMenuActive)
p.isActionBoxMenuHovered = false;
},
/**
* Handles focus event.
* @param {Event} e Focus Event object.
* @private
*/
handleFocus_: function(e) {
if (this.disabled)
return;
if (e.target.parentNode == this) {
// Handles focus event on a large pod.
if (e.target.classList.contains('focused')) {
// Edge case: prevent input box from receiving unncessary focus
// (thus hiding virtual keyboard) when remove user is clicked.
if (e.target.isActionBoxMenuActive)
return;
if (!e.target.multiProfilesPolicyApplied)
e.target.focusInput();
else
e.target.userTypeBubbleElement.classList.add('bubble-shown');
} else
this.focusPod(e.target);
return;
}
// Small pods do not have input box.
if (e.target.parentNode == this.smallPodsContainer) {
this.focusPod(e.target, false, true /* opt_skipInputFocus */);
return;
}
var pod = findAncestorByClass(e.target, 'pod');
if (pod && pod.parentNode == this) {
// Focus on a control of a pod but not on the action area button.
if (!pod.classList.contains('focused')) {
if (e.target.classList.contains('action-box-area') ||
e.target.classList.contains('remove-warning-button')) {
// focusPod usually moves focus on the password input box which
// triggers virtual keyboard to show up. But the focus may move to a
// non text input element shortly by e.target.focus. Hence, a
// virtual keyboard flicking might be observed. We need to manually
// prevent focus on password input box to avoid virtual keyboard
// flicking in this case. See crbug.com/396016 for details.
this.focusPod(pod, false, true /* opt_skipInputFocus */);
} else {
this.focusPod(pod);
}
pod.userTypeBubbleElement.classList.remove('bubble-shown');
e.target.focus();
}
return;
}
// Clears pod focus when we reach here. It means new focus is neither
// on a pod nor on a button/input for a pod.
// Do not "defocus" user pod when it is a single pod.
// That means that 'focused' class will not be removed and
// input field/button will always be visible.
if (!this.alwaysFocusSinglePod)
this.focusPod();
else {
// Hide user-type-bubble in case this is one pod and we lost focus of
// it.
this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
}
},
/**
* Handler of keydown event.
* @param {Event} e KeyDown Event object.
*/
handleKeyDown: function(e) {
if (this.disabled)
return;
var editing = e.target.tagName == 'INPUT' && e.target.value;
switch (e.key) {
case 'ArrowLeft':
if (!editing) {
if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
this.focusPod(this.focusedPod_.previousElementSibling);
else
this.focusPod(this.lastElementChild);
e.stopPropagation();
}
break;
case 'ArrowRight':
if (!editing) {
if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
this.focusPod(this.focusedPod_.nextElementSibling);
else
this.focusPod(this.firstElementChild);
e.stopPropagation();
}
break;
case 'Enter':
// Keydown events on public session pods should only be handled by
// their own handler.
if (this.focusedPod_ && !this.focusedPod_.isPublicSessionPod) {
var targetTag = e.target.tagName;
if (e.target == this.focusedPod_.passwordElement ||
(this.focusedPod_.pinKeyboard &&
e.target == this.focusedPod_.pinKeyboard.inputElement) ||
(targetTag != 'INPUT' &&
targetTag != 'BUTTON' &&
targetTag != 'A')) {
this.setActivatedPod(this.focusedPod_, e);
e.stopPropagation();
}
}
break;
case 'Escape':
if (!this.alwaysFocusSinglePod)
this.focusPod();
break;
}
if (this.isValidInPassword(e.key)) {
if (!this.focusedPod_ && this.mainPod_) {
// If no pod is being focused, and a valid password key is entered,
// move focus to the input field of the main pod. The key will be
// treated as the first character of the password. Please note: when
// the focus is on the header bar (excluding the status tray area),
// this will also move focus to the main pod.
this.focusPod(this.mainPod_);
} else if (this.focusedPod_) {
// If there's a focused pod but its input field is not focused (e.g.
// when dropdown menu is shown, or crbug.com/725622), move focus to
// the input field which will treat the key as password. This is a
// no-op for small pods, or if the input field is already focused.
this.focusedPod_.focusInput();
}
}
},
/**
* Returns true if the key can be used in a valid password.
* @param {string} key The character to check.
* @return {boolean}
*/
isValidInPassword: function(key) {
// Passwords can consist of any ASCII characters per the guideline:
// https://support.google.com/a/answer/33386. However we'll limit to
// only alphanumeric and some special characters that we are sure won't
// conflict with other keyboard events.
// TODO(wzang): This should ideally be kept in sync with the requirements
// set by the backend.
if (key.length != 1)
return false;
return key.charCodeAt(0) > 31 && key.charCodeAt(0) < 127;
},
/**
* Called right after the pod row is shown.
*/
handleAfterShow: function() {
var focusedPod = this.focusedPod_;
// Without timeout changes in pods positions will be animated even though
// it happened when 'flying-pods' class was disabled.
setTimeout(function() {
Oobe.getInstance().toggleClass('flying-pods', true);
if (focusedPod)
ensureTransitionEndEvent(focusedPod);
}, 0);
// Force input focus for user pod on show and once transition ends.
if (focusedPod) {
var screen = this.parentNode;
var self = this;
focusedPod.addEventListener('transitionend', function f(e) {
focusedPod.removeEventListener('transitionend', f);
focusedPod.reset(true);
// Notify screen that it is ready.
screen.onShow();
});
}
},
/**
* Called right before the pod row is shown.
*/
handleBeforeShow: function() {
Oobe.getInstance().toggleClass('flying-pods', false);
for (var event in this.listeners_) {
this.ownerDocument.addEventListener(
event, this.listeners_[event][0], this.listeners_[event][1]);
}
$('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
// Header bar should be hidden when virtual keyboard is shown, or
// views-based shelf is shown.
Oobe.getInstance().headerHidden =
this.isScreenShrinked_() || Oobe.getInstance().showingViewsBasedShelf;
if (this.podPlacementPostponed_) {
this.podPlacementPostponed_ = false;
this.placePods_();
this.maybePreselectPod();
}
this.handleAfterPodPlacement_();
},
/**
* Called when the element is hidden.
*/
handleHide: function() {
for (var event in this.listeners_) {
this.ownerDocument.removeEventListener(
event, this.listeners_[event][0], this.listeners_[event][1]);
}
$('login-header-bar').buttonsTabIndex = 0;
// Clear global states that should only applies to account picker.
$('scroll-container').classList.remove('disable-scroll');
$('inner-container').classList.remove('disable-scroll');
$('inner-container').style.top = 'unset';
},
/**
* Called when a pod's user image finishes loading.
*/
handlePodImageLoad: function(pod) {
var index = this.podsWithPendingImages_.indexOf(pod);
if (index == -1) {
return;
}
this.podsWithPendingImages_.splice(index, 1);
if (this.podsWithPendingImages_.length == 0) {
this.classList.remove('images-loading');
this.smallPodsContainer.classList.remove('images-loading');
this.topMask.classList.remove('images-loading');
this.bottomMask.classList.remove('images-loading');
}
},
/**
* Preselects pod, if needed.
*/
maybePreselectPod: function() {
var pod = this.preselectedPod;
// Force a focus update to ensure the correct wallpaper is loaded.
this.focusPod(pod, true /* force */);
// Hide user-type-bubble in case all user pods are disabled and we focus
// first pod.
if (pod && pod.multiProfilesPolicyApplied) {
pod.userTypeBubbleElement.classList.remove('bubble-shown');
}
}
};
return {
PodRow: PodRow
};
});