1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * @fileoverview User pod row implementation. 7 */ 8 9cr.define('login', function() { 10 /** 11 * Number of displayed columns depending on user pod count. 12 * @type {Array.<number>} 13 * @const 14 */ 15 var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6]; 16 17 /** 18 * Mapping between number of columns in pod-row and margin between user pods 19 * for such layout. 20 * @type {Array.<number>} 21 * @const 22 */ 23 var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12]; 24 25 /** 26 * Mapping between number of columns in the desktop pod-row and margin 27 * between user pods for such layout. 28 * @type {Array.<number>} 29 * @const 30 */ 31 var DESKTOP_MARGIN_BY_COLUMNS = [undefined, 15, 15, 15, 15, 15, 15]; 32 33 /** 34 * Maximal number of columns currently supported by pod-row. 35 * @type {number} 36 * @const 37 */ 38 var MAX_NUMBER_OF_COLUMNS = 6; 39 40 /** 41 * Maximal number of rows if sign-in banner is displayed alonside. 42 * @type {number} 43 * @const 44 */ 45 var MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER = 2; 46 47 /** 48 * Variables used for pod placement processing. Width and height should be 49 * synced with computed CSS sizes of pods. 50 */ 51 var POD_WIDTH = 180; 52 var PUBLIC_EXPANDED_WIDTH = 420; 53 var CROS_POD_HEIGHT = 213; 54 var DESKTOP_POD_HEIGHT = 216; 55 var POD_ROW_PADDING = 10; 56 var DESKTOP_ROW_PADDING = 15; 57 58 /** 59 * Minimal padding between user pod and virtual keyboard. 60 * @type {number} 61 * @const 62 */ 63 var USER_POD_KEYBOARD_MIN_PADDING = 20; 64 65 /** 66 * Whether to preselect the first pod automatically on login screen. 67 * @type {boolean} 68 * @const 69 */ 70 var PRESELECT_FIRST_POD = true; 71 72 /** 73 * Maximum time for which the pod row remains hidden until all user images 74 * have been loaded. 75 * @type {number} 76 * @const 77 */ 78 var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000; 79 80 /** 81 * Public session help topic identifier. 82 * @type {number} 83 * @const 84 */ 85 var HELP_TOPIC_PUBLIC_SESSION = 3041033; 86 87 /** 88 * Tab order for user pods. Update these when adding new controls. 89 * @enum {number} 90 * @const 91 */ 92 var UserPodTabOrder = { 93 POD_INPUT: 1, // Password input fields (and whole pods themselves). 94 HEADER_BAR: 2, // Buttons on the header bar (Shutdown, Add User). 95 ACTION_BOX: 3, // Action box buttons. 96 PAD_MENU_ITEM: 4 // User pad menu items (Remove this user). 97 }; 98 99 /** 100 * Supported authentication types. Keep in sync with the enum in 101 * chrome/browser/chromeos/login/login_display.h 102 * @enum {number} 103 * @const 104 */ 105 var AUTH_TYPE = { 106 OFFLINE_PASSWORD: 0, 107 ONLINE_SIGN_IN: 1, 108 NUMERIC_PIN: 2, 109 USER_CLICK: 3, 110 }; 111 112 /** 113 * Names of authentication types. 114 */ 115 var AUTH_TYPE_NAMES = { 116 0: 'offlinePassword', 117 1: 'onlineSignIn', 118 2: 'numericPin', 119 3: 'userClick', 120 }; 121 122 // Focus and tab order are organized as follows: 123 // 124 // (1) all user pods have tab index 1 so they are traversed first; 125 // (2) when a user pod is activated, its tab index is set to -1 and its 126 // main input field gets focus and tab index 1; 127 // (3) buttons on the header bar have tab index 2 so they follow user pods; 128 // (4) Action box buttons have tab index 3 and follow header bar buttons; 129 // (5) lastly, focus jumps to the Status Area and back to user pods. 130 // 131 // 'Focus' event is handled by a capture handler for the whole document 132 // and in some cases 'mousedown' event handlers are used instead of 'click' 133 // handlers where it's necessary to prevent 'focus' event from being fired. 134 135 /** 136 * Helper function to remove a class from given element. 137 * @param {!HTMLElement} el Element whose class list to change. 138 * @param {string} cl Class to remove. 139 */ 140 function removeClass(el, cl) { 141 el.classList.remove(cl); 142 } 143 144 /** 145 * Creates a user pod. 146 * @constructor 147 * @extends {HTMLDivElement} 148 */ 149 var UserPod = cr.ui.define(function() { 150 var node = $('user-pod-template').cloneNode(true); 151 node.removeAttribute('id'); 152 return node; 153 }); 154 155 /** 156 * Stops event propagation from the any user pod child element. 157 * @param {Event} e Event to handle. 158 */ 159 function stopEventPropagation(e) { 160 // Prevent default so that we don't trigger a 'focus' event. 161 e.preventDefault(); 162 e.stopPropagation(); 163 } 164 165 /** 166 * Unique salt added to user image URLs to prevent caching. Dictionary with 167 * user names as keys. 168 * @type {Object} 169 */ 170 UserPod.userImageSalt_ = {}; 171 172 UserPod.prototype = { 173 __proto__: HTMLDivElement.prototype, 174 175 /** @override */ 176 decorate: function() { 177 this.tabIndex = UserPodTabOrder.POD_INPUT; 178 this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX; 179 180 this.addEventListener('keydown', this.handlePodKeyDown_.bind(this)); 181 this.addEventListener('click', this.handleClickOnPod_.bind(this)); 182 183 this.signinButtonElement.addEventListener('click', 184 this.activate.bind(this)); 185 186 this.actionBoxAreaElement.addEventListener('mousedown', 187 stopEventPropagation); 188 this.actionBoxAreaElement.addEventListener('click', 189 this.handleActionAreaButtonClick_.bind(this)); 190 this.actionBoxAreaElement.addEventListener('keydown', 191 this.handleActionAreaButtonKeyDown_.bind(this)); 192 193 this.actionBoxMenuRemoveElement.addEventListener('click', 194 this.handleRemoveCommandClick_.bind(this)); 195 this.actionBoxMenuRemoveElement.addEventListener('keydown', 196 this.handleRemoveCommandKeyDown_.bind(this)); 197 this.actionBoxMenuRemoveElement.addEventListener('blur', 198 this.handleRemoveCommandBlur_.bind(this)); 199 200 if (this.actionBoxRemoveUserWarningButtonElement) { 201 this.actionBoxRemoveUserWarningButtonElement.addEventListener( 202 'click', 203 this.handleRemoveUserConfirmationClick_.bind(this)); 204 } 205 }, 206 207 /** 208 * Initializes the pod after its properties set and added to a pod row. 209 */ 210 initialize: function() { 211 this.passwordElement.addEventListener('keydown', 212 this.parentNode.handleKeyDown.bind(this.parentNode)); 213 this.passwordElement.addEventListener('keypress', 214 this.handlePasswordKeyPress_.bind(this)); 215 216 this.imageElement.addEventListener('load', 217 this.parentNode.handlePodImageLoad.bind(this.parentNode, this)); 218 219 var initialAuthType = this.user.initialAuthType || 220 AUTH_TYPE.OFFLINE_PASSWORD; 221 this.setAuthType(initialAuthType, null); 222 }, 223 224 /** 225 * Resets tab order for pod elements to its initial state. 226 */ 227 resetTabOrder: function() { 228 // Note: the |mainInput| can be the pod itself. 229 this.mainInput.tabIndex = -1; 230 this.tabIndex = UserPodTabOrder.POD_INPUT; 231 }, 232 233 /** 234 * Handles keypress event (i.e. any textual input) on password input. 235 * @param {Event} e Keypress Event object. 236 * @private 237 */ 238 handlePasswordKeyPress_: function(e) { 239 // When tabbing from the system tray a tab key press is received. Suppress 240 // this so as not to type a tab character into the password field. 241 if (e.keyCode == 9) { 242 e.preventDefault(); 243 return; 244 } 245 }, 246 247 /** 248 * Top edge margin number of pixels. 249 * @type {?number} 250 */ 251 set top(top) { 252 this.style.top = cr.ui.toCssPx(top); 253 }, 254 255 /** 256 * Top edge margin number of pixels. 257 */ 258 get top() { 259 return parseInt(this.style.top); 260 }, 261 262 /** 263 * Left edge margin number of pixels. 264 * @type {?number} 265 */ 266 set left(left) { 267 this.style.left = cr.ui.toCssPx(left); 268 }, 269 270 /** 271 * Left edge margin number of pixels. 272 */ 273 get left() { 274 return parseInt(this.style.left); 275 }, 276 277 /** 278 * Height number of pixels. 279 */ 280 get height() { 281 return this.offsetHeight; 282 }, 283 284 /** 285 * Gets signed in indicator element. 286 * @type {!HTMLDivElement} 287 */ 288 get signedInIndicatorElement() { 289 return this.querySelector('.signed-in-indicator'); 290 }, 291 292 /** 293 * Gets image element. 294 * @type {!HTMLImageElement} 295 */ 296 get imageElement() { 297 return this.querySelector('.user-image'); 298 }, 299 300 /** 301 * Gets name element. 302 * @type {!HTMLDivElement} 303 */ 304 get nameElement() { 305 return this.querySelector('.name'); 306 }, 307 308 /** 309 * Gets password field. 310 * @type {!HTMLInputElement} 311 */ 312 get passwordElement() { 313 return this.querySelector('.password'); 314 }, 315 316 /** 317 * Gets the password label, which is used to show a message where the 318 * password field is normally. 319 * @type {!HTMLInputElement} 320 */ 321 get passwordLabelElement() { 322 return this.querySelector('.password-label'); 323 }, 324 325 /** 326 * Gets Caps Lock hint image. 327 * @type {!HTMLImageElement} 328 */ 329 get capslockHintElement() { 330 return this.querySelector('.capslock-hint'); 331 }, 332 333 /** 334 * Gets user sign in button. 335 * @type {!HTMLButtonElement} 336 */ 337 get signinButtonElement() { 338 return this.querySelector('.signin-button'); 339 }, 340 341 /** 342 * Gets launch app button. 343 * @type {!HTMLButtonElement} 344 */ 345 get launchAppButtonElement() { 346 return this.querySelector('.launch-app-button'); 347 }, 348 349 /** 350 * Gets action box area. 351 * @type {!HTMLInputElement} 352 */ 353 get actionBoxAreaElement() { 354 return this.querySelector('.action-box-area'); 355 }, 356 357 /** 358 * Gets user type icon area. 359 * @type {!HTMLDivElement} 360 */ 361 get userTypeIconAreaElement() { 362 return this.querySelector('.user-type-icon-area'); 363 }, 364 365 /** 366 * Gets user type bubble like multi-profiles policy restriction message. 367 * @type {!HTMLDivElement} 368 */ 369 get userTypeBubbleElement() { 370 return this.querySelector('.user-type-bubble'); 371 }, 372 373 /** 374 * Gets user type icon. 375 * @type {!HTMLDivElement} 376 */ 377 get userTypeIconElement() { 378 return this.querySelector('.user-type-icon-image'); 379 }, 380 381 /** 382 * Gets action box menu. 383 * @type {!HTMLInputElement} 384 */ 385 get actionBoxMenuElement() { 386 return this.querySelector('.action-box-menu'); 387 }, 388 389 /** 390 * Gets action box menu title. 391 * @type {!HTMLInputElement} 392 */ 393 get actionBoxMenuTitleElement() { 394 return this.querySelector('.action-box-menu-title'); 395 }, 396 397 /** 398 * Gets action box menu title, user name item. 399 * @type {!HTMLInputElement} 400 */ 401 get actionBoxMenuTitleNameElement() { 402 return this.querySelector('.action-box-menu-title-name'); 403 }, 404 405 /** 406 * Gets action box menu title, user email item. 407 * @type {!HTMLInputElement} 408 */ 409 get actionBoxMenuTitleEmailElement() { 410 return this.querySelector('.action-box-menu-title-email'); 411 }, 412 413 /** 414 * Gets action box menu, remove user command item. 415 * @type {!HTMLInputElement} 416 */ 417 get actionBoxMenuCommandElement() { 418 return this.querySelector('.action-box-menu-remove-command'); 419 }, 420 421 /** 422 * Gets action box menu, remove user command item div. 423 * @type {!HTMLInputElement} 424 */ 425 get actionBoxMenuRemoveElement() { 426 return this.querySelector('.action-box-menu-remove'); 427 }, 428 429 /** 430 * Gets action box menu, remove user warning text div. 431 * @type {!HTMLInputElement} 432 */ 433 get actionBoxRemoveUserWarningTextElement() { 434 return this.querySelector('.action-box-remove-user-warning-text'); 435 }, 436 437 /** 438 * Gets action box menu, remove supervised user warning text div. 439 * @type {!HTMLInputElement} 440 */ 441 get actionBoxRemoveSupervisedUserWarningTextElement() { 442 return this.querySelector( 443 '.action-box-remove-supervised-user-warning-text'); 444 }, 445 446 /** 447 * Gets action box menu, remove user command item div. 448 * @type {!HTMLInputElement} 449 */ 450 get actionBoxRemoveUserWarningElement() { 451 return this.querySelector('.action-box-remove-user-warning'); 452 }, 453 454 /** 455 * Gets action box menu, remove user command item div. 456 * @type {!HTMLInputElement} 457 */ 458 get actionBoxRemoveUserWarningButtonElement() { 459 return this.querySelector('.remove-warning-button'); 460 }, 461 462 /** 463 * Gets the locked user indicator box. 464 * @type {!HTMLInputElement} 465 */ 466 get lockedIndicatorElement() { 467 return this.querySelector('.locked-indicator'); 468 }, 469 470 /** 471 * Gets the supervised user indicator box. 472 * @type {!HTMLInputElement} 473 */ 474 get supervisedUserIndicatorElement() { 475 return this.querySelector('.supervised-indicator'); 476 }, 477 478 /** 479 * Gets the custom icon. This icon is normally hidden, but can be shown 480 * using the chrome.screenlockPrivate API. 481 * @type {!HTMLDivElement} 482 */ 483 get customIconElement() { 484 return this.querySelector('.custom-icon'); 485 }, 486 487 /** 488 * Updates the user pod element. 489 */ 490 update: function() { 491 this.imageElement.src = 'chrome://userimage/' + this.user.username + 492 '?id=' + UserPod.userImageSalt_[this.user.username]; 493 494 this.nameElement.textContent = this.user_.displayName; 495 this.signedInIndicatorElement.hidden = !this.user_.signedIn; 496 497 this.signinButtonElement.hidden = !this.isAuthTypeOnlineSignIn; 498 if (this.isAuthTypeUserClick) 499 this.passwordLabelElement.textContent = this.authValue; 500 501 this.updateActionBoxArea(); 502 503 this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF( 504 'passwordFieldAccessibleName', this.user_.emailAddress)); 505 506 this.customizeUserPodPerUserType(); 507 }, 508 509 updateActionBoxArea: function() { 510 if (this.user_.publicAccount || this.user_.isApp) { 511 this.actionBoxAreaElement.hidden = true; 512 return; 513 } 514 515 this.actionBoxAreaElement.hidden = false; 516 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove; 517 518 this.actionBoxAreaElement.setAttribute( 519 'aria-label', loadTimeData.getStringF( 520 'podMenuButtonAccessibleName', this.user_.emailAddress)); 521 this.actionBoxMenuRemoveElement.setAttribute( 522 'aria-label', loadTimeData.getString( 523 'podMenuRemoveItemAccessibleName')); 524 this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ? 525 loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) : 526 this.user_.displayName; 527 this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress; 528 this.actionBoxMenuTitleEmailElement.hidden = 529 this.user_.locallyManagedUser; 530 531 this.actionBoxMenuCommandElement.textContent = 532 loadTimeData.getString('removeUser'); 533 }, 534 535 customizeUserPodPerUserType: function() { 536 if (this.user_.locallyManagedUser && !this.user_.isDesktopUser) { 537 this.setUserPodIconType('supervised'); 538 } else if (this.multiProfilesPolicyApplied) { 539 // Mark user pod as not focusable which in addition to the grayed out 540 // filter makes it look in disabled state. 541 this.classList.add('not-focusable'); 542 this.setUserPodIconType('policy'); 543 544 this.querySelector('.mp-policy-title').hidden = false; 545 if (this.user.multiProfilesPolicy == 'primary-only') 546 this.querySelector('.mp-policy-primary-only-msg').hidden = false; 547 else if (this.user.multiProfilesPolicy == 'owner-primary-only') 548 this.querySelector('.mp-owner-primary-only-msg').hidden = false; 549 else 550 this.querySelector('.mp-policy-not-allowed-msg').hidden = false; 551 } else if (this.user_.isApp) { 552 this.setUserPodIconType('app'); 553 } 554 }, 555 556 setUserPodIconType: function(userTypeClass) { 557 this.userTypeIconAreaElement.classList.add(userTypeClass); 558 this.userTypeIconAreaElement.hidden = false; 559 }, 560 561 /** 562 * The user that this pod represents. 563 * @type {!Object} 564 */ 565 user_: undefined, 566 get user() { 567 return this.user_; 568 }, 569 set user(userDict) { 570 this.user_ = userDict; 571 this.update(); 572 }, 573 574 /** 575 * Returns true if multi-profiles sign in is currently active and this 576 * user pod is restricted per policy. 577 * @type {boolean} 578 */ 579 get multiProfilesPolicyApplied() { 580 var isMultiProfilesUI = 581 (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING); 582 return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed; 583 }, 584 585 /** 586 * Gets main input element. 587 * @type {(HTMLButtonElement|HTMLInputElement)} 588 */ 589 get mainInput() { 590 if (this.isAuthTypePassword) { 591 return this.passwordElement; 592 } else if (this.isAuthTypeOnlineSignIn) { 593 return this.signinButtonElement; 594 } else if (this.isAuthTypeUserClick) { 595 return this; 596 } 597 }, 598 599 /** 600 * Whether action box button is in active state. 601 * @type {boolean} 602 */ 603 get isActionBoxMenuActive() { 604 return this.actionBoxAreaElement.classList.contains('active'); 605 }, 606 set isActionBoxMenuActive(active) { 607 if (active == this.isActionBoxMenuActive) 608 return; 609 610 if (active) { 611 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove; 612 if (this.actionBoxRemoveUserWarningElement) 613 this.actionBoxRemoveUserWarningElement.hidden = true; 614 615 // Clear focus first if another pod is focused. 616 if (!this.parentNode.isFocused(this)) { 617 this.parentNode.focusPod(undefined, true); 618 this.actionBoxAreaElement.focus(); 619 } 620 this.actionBoxAreaElement.classList.add('active'); 621 } else { 622 this.actionBoxAreaElement.classList.remove('active'); 623 } 624 }, 625 626 /** 627 * Whether action box button is in hovered state. 628 * @type {boolean} 629 */ 630 get isActionBoxMenuHovered() { 631 return this.actionBoxAreaElement.classList.contains('hovered'); 632 }, 633 set isActionBoxMenuHovered(hovered) { 634 if (hovered == this.isActionBoxMenuHovered) 635 return; 636 637 if (hovered) { 638 this.actionBoxAreaElement.classList.add('hovered'); 639 this.classList.add('hovered'); 640 } else { 641 if (this.multiProfilesPolicyApplied) 642 this.userTypeBubbleElement.classList.remove('bubble-shown'); 643 this.actionBoxAreaElement.classList.remove('hovered'); 644 this.classList.remove('hovered'); 645 } 646 }, 647 648 /** 649 * Set the authentication type for the pod. 650 * @param {number} An auth type value defined in the AUTH_TYPE enum. 651 * @param {string} authValue The initial value used for the auth type. 652 */ 653 setAuthType: function(authType, authValue) { 654 this.authType_ = authType; 655 this.authValue_ = authValue; 656 this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]); 657 this.update(); 658 this.reset(this.parentNode.isFocused(this)); 659 }, 660 661 /** 662 * The auth type of the user pod. This value is one of the enum 663 * values in AUTH_TYPE. 664 * @type {number} 665 */ 666 get authType() { 667 return this.authType_; 668 }, 669 670 /** 671 * The initial value used for the pod's authentication type. 672 * eg. a prepopulated password input when using password authentication. 673 */ 674 get authValue() { 675 return this.authValue_; 676 }, 677 678 /** 679 * True if the the user pod uses a password to authenticate. 680 * @type {bool} 681 */ 682 get isAuthTypePassword() { 683 return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD; 684 }, 685 686 /** 687 * True if the the user pod uses a user click to authenticate. 688 * @type {bool} 689 */ 690 get isAuthTypeUserClick() { 691 return this.authType_ == AUTH_TYPE.USER_CLICK; 692 }, 693 694 /** 695 * True if the the user pod uses a online sign in to authenticate. 696 * @type {bool} 697 */ 698 get isAuthTypeOnlineSignIn() { 699 return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN; 700 }, 701 702 /** 703 * Updates the image element of the user. 704 */ 705 updateUserImage: function() { 706 UserPod.userImageSalt_[this.user.username] = new Date().getTime(); 707 this.update(); 708 }, 709 710 /** 711 * Focuses on input element. 712 */ 713 focusInput: function() { 714 // Move tabIndex from the whole pod to the main input. 715 // Note: the |mainInput| can be the pod itself. 716 this.tabIndex = -1; 717 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 718 this.mainInput.focus(); 719 }, 720 721 /** 722 * Activates the pod. 723 * @param {Event} e Event object. 724 * @return {boolean} True if activated successfully. 725 */ 726 activate: function(e) { 727 if (this.isAuthTypeOnlineSignIn) { 728 this.showSigninUI(); 729 } else if (this.isAuthTypeUserClick) { 730 Oobe.disableSigninUI(); 731 chrome.send('attemptUnlock', [this.user.username]); 732 } else if (this.isAuthTypePassword) { 733 if (!this.passwordElement.value) 734 return false; 735 Oobe.disableSigninUI(); 736 chrome.send('authenticateUser', 737 [this.user.username, this.passwordElement.value]); 738 } else { 739 console.error('Activating user pod with invalid authentication type: ' + 740 this.authType); 741 } 742 743 return true; 744 }, 745 746 showSupervisedUserSigninWarning: function() { 747 // Locally managed user token has been invalidated. 748 // Make sure that pod is focused i.e. "Sign in" button is seen. 749 this.parentNode.focusPod(this); 750 751 var error = document.createElement('div'); 752 var messageDiv = document.createElement('div'); 753 messageDiv.className = 'error-message-bubble'; 754 messageDiv.textContent = 755 loadTimeData.getString('supervisedUserExpiredTokenWarning'); 756 error.appendChild(messageDiv); 757 758 $('bubble').showContentForElement( 759 this.signinButtonElement, 760 cr.ui.Bubble.Attachment.TOP, 761 error, 762 this.signinButtonElement.offsetWidth / 2, 763 4); 764 }, 765 766 /** 767 * Shows signin UI for this user. 768 */ 769 showSigninUI: function() { 770 if (this.user.locallyManagedUser && !this.user.isDesktopUser) { 771 this.showSupervisedUserSigninWarning(); 772 } else { 773 // Special case for multi-profiles sign in. We show users even if they 774 // are not allowed per policy. Restrict those users from starting GAIA. 775 if (this.multiProfilesPolicyApplied) 776 return; 777 778 this.parentNode.showSigninUI(this.user.emailAddress); 779 } 780 }, 781 782 /** 783 * Resets the input field and updates the tab order of pod controls. 784 * @param {boolean} takeFocus If true, input field takes focus. 785 */ 786 reset: function(takeFocus) { 787 this.passwordElement.value = ''; 788 if (takeFocus) 789 this.focusInput(); // This will set a custom tab order. 790 else 791 this.resetTabOrder(); 792 }, 793 794 /** 795 * Handles a click event on action area button. 796 * @param {Event} e Click event. 797 */ 798 handleActionAreaButtonClick_: function(e) { 799 if (this.parentNode.disabled) 800 return; 801 this.isActionBoxMenuActive = !this.isActionBoxMenuActive; 802 e.stopPropagation(); 803 }, 804 805 /** 806 * Handles a keydown event on action area button. 807 * @param {Event} e KeyDown event. 808 */ 809 handleActionAreaButtonKeyDown_: function(e) { 810 if (this.disabled) 811 return; 812 switch (e.keyIdentifier) { 813 case 'Enter': 814 case 'U+0020': // Space 815 if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive) 816 this.isActionBoxMenuActive = true; 817 e.stopPropagation(); 818 break; 819 case 'Up': 820 case 'Down': 821 if (this.isActionBoxMenuActive) { 822 this.actionBoxMenuRemoveElement.tabIndex = 823 UserPodTabOrder.PAD_MENU_ITEM; 824 this.actionBoxMenuRemoveElement.focus(); 825 } 826 e.stopPropagation(); 827 break; 828 case 'U+001B': // Esc 829 this.isActionBoxMenuActive = false; 830 e.stopPropagation(); 831 break; 832 case 'U+0009': // Tab 833 this.parentNode.focusPod(); 834 default: 835 this.isActionBoxMenuActive = false; 836 break; 837 } 838 }, 839 840 /** 841 * Handles a click event on remove user command. 842 * @param {Event} e Click event. 843 */ 844 handleRemoveCommandClick_: function(e) { 845 if (this.user.locallyManagedUser || this.user.isDesktopUser) { 846 this.showRemoveWarning_(); 847 return; 848 } 849 if (this.isActionBoxMenuActive) 850 chrome.send('removeUser', [this.user.username]); 851 }, 852 853 /** 854 * Shows remove user warning. Used for supervised users on CrOS, and for all 855 * users on desktop. 856 */ 857 showRemoveWarning_: function() { 858 this.actionBoxMenuRemoveElement.hidden = true; 859 this.actionBoxRemoveUserWarningElement.hidden = false; 860 }, 861 862 /** 863 * Handles a click event on remove user confirmation button. 864 * @param {Event} e Click event. 865 */ 866 handleRemoveUserConfirmationClick_: function(e) { 867 if (this.isActionBoxMenuActive) 868 chrome.send('removeUser', [this.user.username]); 869 }, 870 871 /** 872 * Handles a keydown event on remove command. 873 * @param {Event} e KeyDown event. 874 */ 875 handleRemoveCommandKeyDown_: function(e) { 876 if (this.disabled) 877 return; 878 switch (e.keyIdentifier) { 879 case 'Enter': 880 chrome.send('removeUser', [this.user.username]); 881 e.stopPropagation(); 882 break; 883 case 'Up': 884 case 'Down': 885 e.stopPropagation(); 886 break; 887 case 'U+001B': // Esc 888 this.actionBoxAreaElement.focus(); 889 this.isActionBoxMenuActive = false; 890 e.stopPropagation(); 891 break; 892 default: 893 this.actionBoxAreaElement.focus(); 894 this.isActionBoxMenuActive = false; 895 break; 896 } 897 }, 898 899 /** 900 * Handles a blur event on remove command. 901 * @param {Event} e Blur event. 902 */ 903 handleRemoveCommandBlur_: function(e) { 904 if (this.disabled) 905 return; 906 this.actionBoxMenuRemoveElement.tabIndex = -1; 907 }, 908 909 /** 910 * Handles click event on a user pod. 911 * @param {Event} e Click event. 912 */ 913 handleClickOnPod_: function(e) { 914 if (this.parentNode.disabled) 915 return; 916 917 if (!this.isActionBoxMenuActive) { 918 if (this.isAuthTypeOnlineSignIn) { 919 this.showSigninUI(); 920 } else if (this.isAuthTypeUserClick) { 921 this.parentNode.setActivatedPod(this); 922 } 923 924 if (this.multiProfilesPolicyApplied) 925 this.userTypeBubbleElement.classList.add('bubble-shown'); 926 927 // Prevent default so that we don't trigger 'focus' event. 928 e.preventDefault(); 929 } 930 }, 931 932 /** 933 * Handles keydown event for a user pod. 934 * @param {Event} e Key event. 935 */ 936 handlePodKeyDown_: function(e) { 937 if (!this.isAuthTypeUserClick || this.disabled) 938 return; 939 switch (e.keyIdentifier) { 940 case 'Enter': 941 case 'U+0020': // Space 942 if (this.parentNode.isFocused(this)) 943 this.parentNode.setActivatedPod(this); 944 break; 945 } 946 } 947 }; 948 949 /** 950 * Creates a public account user pod. 951 * @constructor 952 * @extends {UserPod} 953 */ 954 var PublicAccountUserPod = cr.ui.define(function() { 955 var node = UserPod(); 956 957 var extras = $('public-account-user-pod-extras-template').children; 958 for (var i = 0; i < extras.length; ++i) { 959 var el = extras[i].cloneNode(true); 960 node.appendChild(el); 961 } 962 963 return node; 964 }); 965 966 PublicAccountUserPod.prototype = { 967 __proto__: UserPod.prototype, 968 969 /** 970 * "Enter" button in expanded side pane. 971 * @type {!HTMLButtonElement} 972 */ 973 get enterButtonElement() { 974 return this.querySelector('.enter-button'); 975 }, 976 977 /** 978 * Boolean flag of whether the pod is showing the side pane. The flag 979 * controls whether 'expanded' class is added to the pod's class list and 980 * resets tab order because main input element changes when the 'expanded' 981 * state changes. 982 * @type {boolean} 983 */ 984 get expanded() { 985 return this.classList.contains('expanded'); 986 }, 987 988 /** 989 * During transition final height of pod is not available because of 990 * flexbox layout. That's why we have to calculate 991 * the final height manually. 992 */ 993 get expandedHeight_() { 994 function getTopAndBottomPadding(domElement) { 995 return parseInt(window.getComputedStyle( 996 domElement).getPropertyValue('padding-top')) + 997 parseInt(window.getComputedStyle( 998 domElement).getPropertyValue('padding-bottom')); 999 }; 1000 var height = 1001 this.getElementsByClassName('side-pane-contents')[0].offsetHeight + 1002 this.getElementsByClassName('enter-button')[0].offsetHeight + 1003 getTopAndBottomPadding( 1004 this.getElementsByClassName('enter-button')[0]) + 1005 getTopAndBottomPadding( 1006 this.getElementsByClassName('side-pane-container')[0]) + 1007 getTopAndBottomPadding(this); 1008 return height; 1009 }, 1010 1011 set expanded(expanded) { 1012 if (this.expanded == expanded) 1013 return; 1014 1015 this.resetTabOrder(); 1016 this.classList.toggle('expanded', expanded); 1017 if (expanded) { 1018 var isDesktopUserManager = Oobe.getInstance().displayType == 1019 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 1020 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : 1021 POD_ROW_PADDING; 1022 this.usualLeft = this.left; 1023 this.usualTop = this.top; 1024 if (this.left + PUBLIC_EXPANDED_WIDTH > 1025 $('pod-row').offsetWidth - rowPadding) 1026 this.left = $('pod-row').offsetWidth - rowPadding - 1027 PUBLIC_EXPANDED_WIDTH; 1028 var expandedHeight = this.expandedHeight_; 1029 if (this.top + expandedHeight > $('pod-row').offsetHeight) 1030 this.top = $('pod-row').offsetHeight - expandedHeight; 1031 } else { 1032 if (typeof(this.usualLeft) != 'undefined') 1033 this.left = this.usualLeft; 1034 if (typeof(this.usualTop) != 'undefined') 1035 this.top = this.usualTop; 1036 } 1037 1038 var self = this; 1039 this.classList.add('animating'); 1040 this.addEventListener('webkitTransitionEnd', function f(e) { 1041 self.removeEventListener('webkitTransitionEnd', f); 1042 self.classList.remove('animating'); 1043 1044 // Accessibility focus indicator does not move with the focused 1045 // element. Sends a 'focus' event on the currently focused element 1046 // so that accessibility focus indicator updates its location. 1047 if (document.activeElement) 1048 document.activeElement.dispatchEvent(new Event('focus')); 1049 }); 1050 }, 1051 1052 /** @override */ 1053 get mainInput() { 1054 if (this.expanded) 1055 return this.enterButtonElement; 1056 else 1057 return this.nameElement; 1058 }, 1059 1060 /** @override */ 1061 decorate: function() { 1062 UserPod.prototype.decorate.call(this); 1063 1064 this.classList.remove('need-password'); 1065 this.classList.add('public-account'); 1066 1067 this.nameElement.addEventListener('keydown', (function(e) { 1068 if (e.keyIdentifier == 'Enter') { 1069 this.parentNode.setActivatedPod(this, e); 1070 // Stop this keydown event from bubbling up to PodRow handler. 1071 e.stopPropagation(); 1072 // Prevent default so that we don't trigger a 'click' event on the 1073 // newly focused "Enter" button. 1074 e.preventDefault(); 1075 } 1076 }).bind(this)); 1077 1078 var learnMore = this.querySelector('.learn-more'); 1079 learnMore.addEventListener('mousedown', stopEventPropagation); 1080 learnMore.addEventListener('click', this.handleLearnMoreEvent); 1081 learnMore.addEventListener('keydown', this.handleLearnMoreEvent); 1082 1083 learnMore = this.querySelector('.side-pane-learn-more'); 1084 learnMore.addEventListener('click', this.handleLearnMoreEvent); 1085 learnMore.addEventListener('keydown', this.handleLearnMoreEvent); 1086 1087 this.enterButtonElement.addEventListener('click', (function(e) { 1088 this.enterButtonElement.disabled = true; 1089 chrome.send('launchPublicAccount', [this.user.username]); 1090 }).bind(this)); 1091 }, 1092 1093 /** @override **/ 1094 update: function() { 1095 UserPod.prototype.update.call(this); 1096 this.querySelector('.side-pane-name').textContent = 1097 this.user_.displayName; 1098 this.querySelector('.info').textContent = 1099 loadTimeData.getStringF('publicAccountInfoFormat', 1100 this.user_.enterpriseDomain); 1101 }, 1102 1103 /** @override */ 1104 focusInput: function() { 1105 // Move tabIndex from the whole pod to the main input. 1106 this.tabIndex = -1; 1107 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 1108 this.mainInput.focus(); 1109 }, 1110 1111 /** @override */ 1112 reset: function(takeFocus) { 1113 if (!takeFocus) 1114 this.expanded = false; 1115 this.enterButtonElement.disabled = false; 1116 UserPod.prototype.reset.call(this, takeFocus); 1117 }, 1118 1119 /** @override */ 1120 activate: function(e) { 1121 this.expanded = true; 1122 this.focusInput(); 1123 return true; 1124 }, 1125 1126 /** @override */ 1127 handleClickOnPod_: function(e) { 1128 if (this.parentNode.disabled) 1129 return; 1130 1131 this.parentNode.focusPod(this); 1132 this.parentNode.setActivatedPod(this, e); 1133 // Prevent default so that we don't trigger 'focus' event. 1134 e.preventDefault(); 1135 }, 1136 1137 /** 1138 * Handle mouse and keyboard events for the learn more button. Triggering 1139 * the button causes information about public sessions to be shown. 1140 * @param {Event} event Mouse or keyboard event. 1141 */ 1142 handleLearnMoreEvent: function(event) { 1143 switch (event.type) { 1144 // Show informaton on left click. Let any other clicks propagate. 1145 case 'click': 1146 if (event.button != 0) 1147 return; 1148 break; 1149 // Show informaton when <Return> or <Space> is pressed. Let any other 1150 // key presses propagate. 1151 case 'keydown': 1152 switch (event.keyCode) { 1153 case 13: // Return. 1154 case 32: // Space. 1155 break; 1156 default: 1157 return; 1158 } 1159 break; 1160 } 1161 chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]); 1162 stopEventPropagation(event); 1163 }, 1164 }; 1165 1166 /** 1167 * Creates a user pod to be used only in desktop chrome. 1168 * @constructor 1169 * @extends {UserPod} 1170 */ 1171 var DesktopUserPod = cr.ui.define(function() { 1172 // Don't just instantiate a UserPod(), as this will call decorate() on the 1173 // parent object, and add duplicate event listeners. 1174 var node = $('user-pod-template').cloneNode(true); 1175 node.removeAttribute('id'); 1176 return node; 1177 }); 1178 1179 DesktopUserPod.prototype = { 1180 __proto__: UserPod.prototype, 1181 1182 /** @override */ 1183 get mainInput() { 1184 if (!this.passwordElement.hidden) 1185 return this.passwordElement; 1186 else 1187 return this.nameElement; 1188 }, 1189 1190 /** @override */ 1191 decorate: function() { 1192 UserPod.prototype.decorate.call(this); 1193 }, 1194 1195 /** @override */ 1196 update: function() { 1197 this.imageElement.src = this.user.userImage; 1198 this.nameElement.textContent = this.user.displayName; 1199 1200 var isLockedUser = this.user.needsSignin; 1201 var isSupervisedUser = this.user.locallyManagedUser; 1202 this.signinButtonElement.hidden = true; 1203 this.lockedIndicatorElement.hidden = !isLockedUser; 1204 this.supervisedUserIndicatorElement.hidden = !isSupervisedUser; 1205 this.passwordElement.hidden = !isLockedUser; 1206 this.nameElement.hidden = isLockedUser; 1207 1208 if (this.isAuthTypeUserClick) 1209 this.passwordLabelElement.textContent = this.authValue; 1210 1211 this.actionBoxRemoveUserWarningTextElement.hidden = isSupervisedUser; 1212 this.actionBoxRemoveSupervisedUserWarningTextElement.hidden = 1213 !isSupervisedUser; 1214 1215 UserPod.prototype.updateActionBoxArea.call(this); 1216 }, 1217 1218 /** @override */ 1219 focusInput: function() { 1220 // For focused pods, display the name unless the pod is locked. 1221 var isLockedUser = this.user.needsSignin; 1222 var isSupervisedUser = this.user.locallyManagedUser; 1223 this.signinButtonElement.hidden = true; 1224 this.lockedIndicatorElement.hidden = !isLockedUser; 1225 this.supervisedUserIndicatorElement.hidden = !isSupervisedUser; 1226 this.passwordElement.hidden = !isLockedUser; 1227 this.nameElement.hidden = isLockedUser; 1228 1229 // Move tabIndex from the whole pod to the main input. 1230 this.tabIndex = -1; 1231 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 1232 this.mainInput.focus(); 1233 }, 1234 1235 /** @override */ 1236 reset: function(takeFocus) { 1237 // Always display the user's name for unfocused pods. 1238 if (!takeFocus) 1239 this.nameElement.hidden = false; 1240 UserPod.prototype.reset.call(this, takeFocus); 1241 }, 1242 1243 /** @override */ 1244 activate: function(e) { 1245 if (this.passwordElement.hidden) { 1246 Oobe.launchUser(this.user.emailAddress, this.user.displayName); 1247 } else if (!this.passwordElement.value) { 1248 return false; 1249 } else { 1250 chrome.send('authenticatedLaunchUser', 1251 [this.user.emailAddress, 1252 this.user.displayName, 1253 this.passwordElement.value]); 1254 } 1255 this.passwordElement.value = ''; 1256 return true; 1257 }, 1258 1259 /** @override */ 1260 handleClickOnPod_: function(e) { 1261 if (this.parentNode.disabled) 1262 return; 1263 1264 Oobe.clearErrors(); 1265 this.parentNode.lastFocusedPod_ = this; 1266 1267 // If this is an unlocked pod, then open a browser window. Otherwise 1268 // just activate the pod and show the password field. 1269 if (!this.user.needsSignin && !this.isActionBoxMenuActive) 1270 this.activate(e); 1271 1272 if (this.isAuthTypeUserClick) 1273 chrome.send('attemptUnlock', [this.user.emailAddress]); 1274 }, 1275 1276 /** @override */ 1277 handleRemoveUserConfirmationClick_: function(e) { 1278 chrome.send('removeUser', [this.user.profilePath]); 1279 }, 1280 }; 1281 1282 /** 1283 * Creates a user pod that represents kiosk app. 1284 * @constructor 1285 * @extends {UserPod} 1286 */ 1287 var KioskAppPod = cr.ui.define(function() { 1288 var node = UserPod(); 1289 return node; 1290 }); 1291 1292 KioskAppPod.prototype = { 1293 __proto__: UserPod.prototype, 1294 1295 /** @override */ 1296 decorate: function() { 1297 UserPod.prototype.decorate.call(this); 1298 this.launchAppButtonElement.addEventListener('click', 1299 this.activate.bind(this)); 1300 }, 1301 1302 /** @override */ 1303 update: function() { 1304 this.imageElement.src = this.user.iconUrl; 1305 if (this.user.iconHeight && this.user.iconWidth) { 1306 this.imageElement.style.height = this.user.iconHeight; 1307 this.imageElement.style.width = this.user.iconWidth; 1308 } 1309 this.imageElement.alt = this.user.label; 1310 this.imageElement.title = this.user.label; 1311 this.passwordElement.hidden = true; 1312 this.signinButtonElement.hidden = true; 1313 this.launchAppButtonElement.hidden = false; 1314 this.signedInIndicatorElement.hidden = true; 1315 this.nameElement.textContent = this.user.label; 1316 1317 UserPod.prototype.updateActionBoxArea.call(this); 1318 UserPod.prototype.customizeUserPodPerUserType.call(this); 1319 }, 1320 1321 /** @override */ 1322 get mainInput() { 1323 return this.launchAppButtonElement; 1324 }, 1325 1326 /** @override */ 1327 focusInput: function() { 1328 this.signinButtonElement.hidden = true; 1329 this.launchAppButtonElement.hidden = false; 1330 this.passwordElement.hidden = true; 1331 1332 // Move tabIndex from the whole pod to the main input. 1333 this.tabIndex = -1; 1334 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 1335 this.mainInput.focus(); 1336 }, 1337 1338 /** @override */ 1339 get forceOnlineSignin() { 1340 return false; 1341 }, 1342 1343 /** @override */ 1344 activate: function(e) { 1345 var diagnosticMode = e && e.ctrlKey; 1346 this.launchApp_(this.user, diagnosticMode); 1347 return true; 1348 }, 1349 1350 /** @override */ 1351 handleClickOnPod_: function(e) { 1352 if (this.parentNode.disabled) 1353 return; 1354 1355 Oobe.clearErrors(); 1356 this.parentNode.lastFocusedPod_ = this; 1357 this.activate(e); 1358 }, 1359 1360 /** 1361 * Launch the app. If |diagnosticMode| is true, ask user to confirm. 1362 * @param {Object} app App data. 1363 * @param {boolean} diagnosticMode Whether to run the app in diagnostic 1364 * mode. 1365 */ 1366 launchApp_: function(app, diagnosticMode) { 1367 if (!diagnosticMode) { 1368 chrome.send('launchKioskApp', [app.id, false]); 1369 return; 1370 } 1371 1372 var oobe = $('oobe'); 1373 if (!oobe.confirmDiagnosticMode_) { 1374 oobe.confirmDiagnosticMode_ = 1375 new cr.ui.dialogs.ConfirmDialog(document.body); 1376 oobe.confirmDiagnosticMode_.setOkLabel( 1377 loadTimeData.getString('confirmKioskAppDiagnosticModeYes')); 1378 oobe.confirmDiagnosticMode_.setCancelLabel( 1379 loadTimeData.getString('confirmKioskAppDiagnosticModeNo')); 1380 } 1381 1382 oobe.confirmDiagnosticMode_.show( 1383 loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat', 1384 app.label), 1385 function() { 1386 chrome.send('launchKioskApp', [app.id, true]); 1387 }); 1388 }, 1389 }; 1390 1391 /** 1392 * Creates a new pod row element. 1393 * @constructor 1394 * @extends {HTMLDivElement} 1395 */ 1396 var PodRow = cr.ui.define('podrow'); 1397 1398 PodRow.prototype = { 1399 __proto__: HTMLDivElement.prototype, 1400 1401 // Whether this user pod row is shown for the first time. 1402 firstShown_: true, 1403 1404 // True if inside focusPod(). 1405 insideFocusPod_: false, 1406 1407 // Focused pod. 1408 focusedPod_: undefined, 1409 1410 // Activated pod, i.e. the pod of current login attempt. 1411 activatedPod_: undefined, 1412 1413 // Pod that was most recently focused, if any. 1414 lastFocusedPod_: undefined, 1415 1416 // Pods whose initial images haven't been loaded yet. 1417 podsWithPendingImages_: [], 1418 1419 // Whether pod creation is animated. 1420 userAddIsAnimated_: false, 1421 1422 // Whether pod placement has been postponed. 1423 podPlacementPostponed_: false, 1424 1425 // Standard user pod height/width. 1426 userPodHeight_: 0, 1427 userPodWidth_: 0, 1428 1429 // Array of apps that are shown in addition to other user pods. 1430 apps_: [], 1431 1432 // True to show app pods along with user pods. 1433 shouldShowApps_: true, 1434 1435 // Array of users that are shown (public/supervised/regular). 1436 users_: [], 1437 1438 /** @override */ 1439 decorate: function() { 1440 // Event listeners that are installed for the time period during which 1441 // the element is visible. 1442 this.listeners_ = { 1443 focus: [this.handleFocus_.bind(this), true /* useCapture */], 1444 click: [this.handleClick_.bind(this), true], 1445 mousemove: [this.handleMouseMove_.bind(this), false], 1446 keydown: [this.handleKeyDown.bind(this), false] 1447 }; 1448 1449 var isDesktopUserManager = Oobe.getInstance().displayType == 1450 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 1451 this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT : 1452 CROS_POD_HEIGHT; 1453 // Same for Chrome OS and desktop. 1454 this.userPodWidth_ = POD_WIDTH; 1455 }, 1456 1457 /** 1458 * Returns all the pods in this pod row. 1459 * @type {NodeList} 1460 */ 1461 get pods() { 1462 return Array.prototype.slice.call(this.children); 1463 }, 1464 1465 /** 1466 * Return true if user pod row has only single user pod in it, which should 1467 * always be focused. 1468 * @type {boolean} 1469 */ 1470 get alwaysFocusSinglePod() { 1471 var isDesktopUserManager = Oobe.getInstance().displayType == 1472 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 1473 1474 return isDesktopUserManager ? false : this.children.length == 1; 1475 }, 1476 1477 /** 1478 * Returns pod with the given app id. 1479 * @param {!string} app_id Application id to be matched. 1480 * @return {Object} Pod with the given app id. null if pod hasn't been 1481 * found. 1482 */ 1483 getPodWithAppId_: function(app_id) { 1484 for (var i = 0, pod; pod = this.pods[i]; ++i) { 1485 if (pod.user.isApp && pod.user.id == app_id) 1486 return pod; 1487 } 1488 return null; 1489 }, 1490 1491 /** 1492 * Returns pod with the given username (null if there is no such pod). 1493 * @param {string} username Username to be matched. 1494 * @return {Object} Pod with the given username. null if pod hasn't been 1495 * found. 1496 */ 1497 getPodWithUsername_: function(username) { 1498 for (var i = 0, pod; pod = this.pods[i]; ++i) { 1499 if (pod.user.username == username) 1500 return pod; 1501 } 1502 return null; 1503 }, 1504 1505 /** 1506 * True if the the pod row is disabled (handles no user interaction). 1507 * @type {boolean} 1508 */ 1509 disabled_: false, 1510 get disabled() { 1511 return this.disabled_; 1512 }, 1513 set disabled(value) { 1514 this.disabled_ = value; 1515 var controls = this.querySelectorAll('button,input'); 1516 for (var i = 0, control; control = controls[i]; ++i) { 1517 control.disabled = value; 1518 } 1519 }, 1520 1521 /** 1522 * Creates a user pod from given email. 1523 * @param {!Object} user User info dictionary. 1524 */ 1525 createUserPod: function(user) { 1526 var userPod; 1527 if (user.isDesktopUser) 1528 userPod = new DesktopUserPod({user: user}); 1529 else if (user.publicAccount) 1530 userPod = new PublicAccountUserPod({user: user}); 1531 else if (user.isApp) 1532 userPod = new KioskAppPod({user: user}); 1533 else 1534 userPod = new UserPod({user: user}); 1535 1536 userPod.hidden = false; 1537 return userPod; 1538 }, 1539 1540 /** 1541 * Add an existing user pod to this pod row. 1542 * @param {!Object} user User info dictionary. 1543 * @param {boolean} animated Whether to use init animation. 1544 */ 1545 addUserPod: function(user, animated) { 1546 var userPod = this.createUserPod(user); 1547 if (animated) { 1548 userPod.classList.add('init'); 1549 userPod.nameElement.classList.add('init'); 1550 } 1551 1552 this.appendChild(userPod); 1553 userPod.initialize(); 1554 }, 1555 1556 /** 1557 * Runs app with a given id from the list of loaded apps. 1558 * @param {!string} app_id of an app to run. 1559 * @param {boolean=} opt_diagnostic_mode Whether to run the app in 1560 * diagnostic mode. Default is false. 1561 */ 1562 findAndRunAppForTesting: function(app_id, opt_diagnostic_mode) { 1563 var app = this.getPodWithAppId_(app_id); 1564 if (app) { 1565 var activationEvent = cr.doc.createEvent('MouseEvents'); 1566 var ctrlKey = opt_diagnostic_mode; 1567 activationEvent.initMouseEvent('click', true, true, null, 1568 0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null); 1569 app.dispatchEvent(activationEvent); 1570 } 1571 }, 1572 1573 /** 1574 * Removes user pod from pod row. 1575 * @param {string} email User's email. 1576 */ 1577 removeUserPod: function(username) { 1578 var podToRemove = this.getPodWithUsername_(username); 1579 if (podToRemove == null) { 1580 console.warn('Attempt to remove not existing pod for ' + username + 1581 '.'); 1582 return; 1583 } 1584 this.removeChild(podToRemove); 1585 if (this.pods.length > 0) 1586 this.placePods_(); 1587 }, 1588 1589 /** 1590 * Returns index of given pod or -1 if not found. 1591 * @param {UserPod} pod Pod to look up. 1592 * @private 1593 */ 1594 indexOf_: function(pod) { 1595 for (var i = 0; i < this.pods.length; ++i) { 1596 if (pod == this.pods[i]) 1597 return i; 1598 } 1599 return -1; 1600 }, 1601 1602 /** 1603 * Start first time show animation. 1604 */ 1605 startInitAnimation: function() { 1606 // Schedule init animation. 1607 for (var i = 0, pod; pod = this.pods[i]; ++i) { 1608 window.setTimeout(removeClass, 500 + i * 70, pod, 'init'); 1609 window.setTimeout(removeClass, 700 + i * 70, pod.nameElement, 'init'); 1610 } 1611 }, 1612 1613 /** 1614 * Start login success animation. 1615 */ 1616 startAuthenticatedAnimation: function() { 1617 var activated = this.indexOf_(this.activatedPod_); 1618 if (activated == -1) 1619 return; 1620 1621 for (var i = 0, pod; pod = this.pods[i]; ++i) { 1622 if (i < activated) 1623 pod.classList.add('left'); 1624 else if (i > activated) 1625 pod.classList.add('right'); 1626 else 1627 pod.classList.add('zoom'); 1628 } 1629 }, 1630 1631 /** 1632 * Populates pod row with given existing users and start init animation. 1633 * @param {array} users Array of existing user emails. 1634 * @param {boolean} animated Whether to use init animation. 1635 */ 1636 loadPods: function(users, animated) { 1637 this.users_ = users; 1638 this.userAddIsAnimated_ = animated; 1639 1640 this.rebuildPods(); 1641 }, 1642 1643 /** 1644 * Scrolls focused user pod into view. 1645 */ 1646 scrollFocusedPodIntoView: function() { 1647 var pod = this.focusedPod_; 1648 if (!pod) 1649 return; 1650 1651 // First check whether focused pod is already fully visible. 1652 var visibleArea = $('scroll-container'); 1653 var scrollTop = visibleArea.scrollTop; 1654 var clientHeight = visibleArea.clientHeight; 1655 var podTop = $('oobe').offsetTop + pod.offsetTop; 1656 var padding = USER_POD_KEYBOARD_MIN_PADDING; 1657 if (podTop + pod.height + padding <= scrollTop + clientHeight && 1658 podTop - padding >= scrollTop) { 1659 return; 1660 } 1661 1662 // Scroll so that user pod is as centered as possible. 1663 visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2; 1664 }, 1665 1666 /** 1667 * Rebuilds pod row using users_ and apps_ that were previously set or 1668 * updated. 1669 */ 1670 rebuildPods: function() { 1671 var emptyPodRow = this.pods.length == 0; 1672 1673 // Clear existing pods. 1674 this.innerHTML = ''; 1675 this.focusedPod_ = undefined; 1676 this.activatedPod_ = undefined; 1677 this.lastFocusedPod_ = undefined; 1678 1679 // Switch off animation 1680 Oobe.getInstance().toggleClass('flying-pods', false); 1681 1682 // Populate the pod row. 1683 for (var i = 0; i < this.users_.length; ++i) 1684 this.addUserPod(this.users_[i], this.userAddIsAnimated_); 1685 1686 for (var i = 0, pod; pod = this.pods[i]; ++i) 1687 this.podsWithPendingImages_.push(pod); 1688 1689 // TODO(nkostylev): Edge case handling when kiosk apps are not fitting. 1690 if (this.shouldShowApps_) { 1691 for (var i = 0; i < this.apps_.length; ++i) 1692 this.addUserPod(this.apps_[i], this.userAddIsAnimated_); 1693 } 1694 1695 // Make sure we eventually show the pod row, even if some image is stuck. 1696 setTimeout(function() { 1697 $('pod-row').classList.remove('images-loading'); 1698 }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS); 1699 1700 var isCrosAccountPicker = $('login-header-bar').signinUIState == 1701 SIGNIN_UI_STATE.ACCOUNT_PICKER; 1702 var isDesktopUserManager = Oobe.getInstance().displayType == 1703 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 1704 1705 // Chrome OS: immediately recalculate pods layout only when current UI 1706 // is account picker. Otherwise postpone it. 1707 // Desktop: recalculate pods layout right away. 1708 if (isDesktopUserManager || isCrosAccountPicker) { 1709 this.placePods_(); 1710 1711 // Without timeout changes in pods positions will be animated even 1712 // though it happened when 'flying-pods' class was disabled. 1713 setTimeout(function() { 1714 Oobe.getInstance().toggleClass('flying-pods', true); 1715 }, 0); 1716 1717 // On desktop, don't pre-select a pod if it's the only one. 1718 if (isDesktopUserManager && this.pods.length == 1) 1719 this.focusPod(); 1720 else 1721 this.focusPod(this.preselectedPod); 1722 } else { 1723 this.podPlacementPostponed_ = true; 1724 1725 // Update [Cancel] button state. 1726 if ($('login-header-bar').signinUIState == 1727 SIGNIN_UI_STATE.GAIA_SIGNIN && 1728 emptyPodRow && 1729 this.pods.length > 0) { 1730 login.GaiaSigninScreen.updateCancelButtonState(); 1731 } 1732 } 1733 }, 1734 1735 /** 1736 * Adds given apps to the pod row. 1737 * @param {array} apps Array of apps. 1738 */ 1739 setApps: function(apps) { 1740 this.apps_ = apps; 1741 this.rebuildPods(); 1742 chrome.send('kioskAppsLoaded'); 1743 1744 // Check whether there's a pending kiosk app error. 1745 window.setTimeout(function() { 1746 chrome.send('checkKioskAppLaunchError'); 1747 }, 500); 1748 }, 1749 1750 /** 1751 * Sets whether should show app pods. 1752 * @param {boolean} shouldShowApps Whether app pods should be shown. 1753 */ 1754 setShouldShowApps: function(shouldShowApps) { 1755 if (this.shouldShowApps_ == shouldShowApps) 1756 return; 1757 1758 this.shouldShowApps_ = shouldShowApps; 1759 this.rebuildPods(); 1760 }, 1761 1762 /** 1763 * Shows a custom icon on a user pod besides the input field. 1764 * @param {string} username Username of pod to add button 1765 * @param {{scale1x: string, scale2x: string}} icon Dictionary of URLs of 1766 * the custom icon's representations for 1x and 2x scale factors. 1767 */ 1768 showUserPodCustomIcon: function(username, icon) { 1769 var pod = this.getPodWithUsername_(username); 1770 if (pod == null) { 1771 console.error('Unable to show user pod button for ' + username + 1772 ': user pod not found.'); 1773 return; 1774 } 1775 1776 pod.customIconElement.hidden = false; 1777 pod.customIconElement.style.backgroundImage = 1778 '-webkit-image-set(' + 1779 'url(' + icon.scale1x + ') 1x,' + 1780 'url(' + icon.scale2x + ') 2x)'; 1781 }, 1782 1783 /** 1784 * Hides the custom icon in the user pod added by showUserPodCustomIcon(). 1785 * @param {string} username Username of pod to remove button 1786 */ 1787 hideUserPodCustomIcon: function(username) { 1788 var pod = this.getPodWithUsername_(username); 1789 if (pod == null) { 1790 console.error('Unable to hide user pod button for ' + username + 1791 ': user pod not found.'); 1792 return; 1793 } 1794 1795 pod.customIconElement.hidden = true; 1796 }, 1797 1798 /** 1799 * Sets the authentication type used to authenticate the user. 1800 * @param {string} username Username of selected user 1801 * @param {number} authType Authentication type, must be one of the 1802 * values listed in AUTH_TYPE enum. 1803 * @param {string} value The initial value to use for authentication. 1804 */ 1805 setAuthType: function(username, authType, value) { 1806 var pod = this.getPodWithUsername_(username); 1807 if (pod == null) { 1808 console.error('Unable to set auth type for ' + username + 1809 ': user pod not found.'); 1810 return; 1811 } 1812 pod.setAuthType(authType, value); 1813 }, 1814 1815 /** 1816 * Shows a tooltip bubble explaining Easy Unlock for the focused pod. 1817 */ 1818 showEasyUnlockBubble: function() { 1819 if (!this.focusedPod_) { 1820 console.error('No focused pod to show Easy Unlock bubble.'); 1821 return; 1822 } 1823 1824 var bubbleContent = document.createElement('div'); 1825 bubbleContent.classList.add('easy-unlock-button-content'); 1826 bubbleContent.textContent = loadTimeData.getString('easyUnlockTooltip'); 1827 1828 var attachElement = this.focusedPod_.customIconElement; 1829 /** @const */ var BUBBLE_OFFSET = 20; 1830 /** @const */ var BUBBLE_PADDING = 8; 1831 $('bubble').showContentForElement(attachElement, 1832 cr.ui.Bubble.Attachment.RIGHT, 1833 bubbleContent, 1834 BUBBLE_OFFSET, 1835 BUBBLE_PADDING); 1836 }, 1837 1838 /** 1839 * Called when window was resized. 1840 */ 1841 onWindowResize: function() { 1842 var layout = this.calculateLayout_(); 1843 if (layout.columns != this.columns || layout.rows != this.rows) 1844 this.placePods_(); 1845 1846 if (Oobe.getInstance().virtualKeyboardShown) 1847 this.scrollFocusedPodIntoView(); 1848 }, 1849 1850 /** 1851 * Returns width of podrow having |columns| number of columns. 1852 * @private 1853 */ 1854 columnsToWidth_: function(columns) { 1855 var isDesktopUserManager = Oobe.getInstance().displayType == 1856 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 1857 var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] : 1858 MARGIN_BY_COLUMNS[columns]; 1859 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : 1860 POD_ROW_PADDING; 1861 return 2 * rowPadding + columns * this.userPodWidth_ + 1862 (columns - 1) * margin; 1863 }, 1864 1865 /** 1866 * Returns height of podrow having |rows| number of rows. 1867 * @private 1868 */ 1869 rowsToHeight_: function(rows) { 1870 var isDesktopUserManager = Oobe.getInstance().displayType == 1871 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 1872 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : 1873 POD_ROW_PADDING; 1874 return 2 * rowPadding + rows * this.userPodHeight_; 1875 }, 1876 1877 /** 1878 * Calculates number of columns and rows that podrow should have in order to 1879 * hold as much its pods as possible for current screen size. Also it tries 1880 * to choose layout that looks good. 1881 * @return {{columns: number, rows: number}} 1882 */ 1883 calculateLayout_: function() { 1884 var preferredColumns = this.pods.length < COLUMNS.length ? 1885 COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1]; 1886 var maxWidth = Oobe.getInstance().clientAreaSize.width; 1887 var columns = preferredColumns; 1888 while (maxWidth < this.columnsToWidth_(columns) && columns > 1) 1889 --columns; 1890 var rows = Math.floor((this.pods.length - 1) / columns) + 1; 1891 if (getComputedStyle( 1892 $('signin-banner'), null).getPropertyValue('display') != 'none') { 1893 rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER); 1894 } 1895 var maxHeigth = Oobe.getInstance().clientAreaSize.height; 1896 while (maxHeigth < this.rowsToHeight_(rows) && rows > 1) 1897 --rows; 1898 // One more iteration if it's not enough cells to place all pods. 1899 while (maxWidth >= this.columnsToWidth_(columns + 1) && 1900 columns * rows < this.pods.length && 1901 columns < MAX_NUMBER_OF_COLUMNS) { 1902 ++columns; 1903 } 1904 return {columns: columns, rows: rows}; 1905 }, 1906 1907 /** 1908 * Places pods onto their positions onto pod grid. 1909 * @private 1910 */ 1911 placePods_: function() { 1912 var layout = this.calculateLayout_(); 1913 var columns = this.columns = layout.columns; 1914 var rows = this.rows = layout.rows; 1915 var maxPodsNumber = columns * rows; 1916 var isDesktopUserManager = Oobe.getInstance().displayType == 1917 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 1918 var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] : 1919 MARGIN_BY_COLUMNS[columns]; 1920 this.parentNode.setPreferredSize( 1921 this.columnsToWidth_(columns), this.rowsToHeight_(rows)); 1922 var height = this.userPodHeight_; 1923 var width = this.userPodWidth_; 1924 this.pods.forEach(function(pod, index) { 1925 if (pod.offsetHeight != height) { 1926 console.error('Pod offsetHeight (' + pod.offsetHeight + 1927 ') and POD_HEIGHT (' + height + ') are not equal.'); 1928 } 1929 if (pod.offsetWidth != width) { 1930 console.error('Pod offsetWidth (' + pod.offsetWidth + 1931 ') and POD_WIDTH (' + width + ') are not equal.'); 1932 } 1933 if (index >= maxPodsNumber) { 1934 pod.hidden = true; 1935 return; 1936 } 1937 pod.hidden = false; 1938 var column = index % columns; 1939 var row = Math.floor(index / columns); 1940 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : 1941 POD_ROW_PADDING; 1942 pod.left = rowPadding + column * (width + margin); 1943 1944 // On desktop, we want the rows to always be equally spaced. 1945 pod.top = isDesktopUserManager ? row * (height + rowPadding) : 1946 row * height + rowPadding; 1947 }); 1948 Oobe.getInstance().updateScreenSize(this.parentNode); 1949 }, 1950 1951 /** 1952 * Number of columns. 1953 * @type {?number} 1954 */ 1955 set columns(columns) { 1956 // Cannot use 'columns' here. 1957 this.setAttribute('ncolumns', columns); 1958 }, 1959 get columns() { 1960 return parseInt(this.getAttribute('ncolumns')); 1961 }, 1962 1963 /** 1964 * Number of rows. 1965 * @type {?number} 1966 */ 1967 set rows(rows) { 1968 // Cannot use 'rows' here. 1969 this.setAttribute('nrows', rows); 1970 }, 1971 get rows() { 1972 return parseInt(this.getAttribute('nrows')); 1973 }, 1974 1975 /** 1976 * Whether the pod is currently focused. 1977 * @param {UserPod} pod Pod to check for focus. 1978 * @return {boolean} Pod focus status. 1979 */ 1980 isFocused: function(pod) { 1981 return this.focusedPod_ == pod; 1982 }, 1983 1984 /** 1985 * Focuses a given user pod or clear focus when given null. 1986 * @param {UserPod=} podToFocus User pod to focus (undefined clears focus). 1987 * @param {boolean=} opt_force If true, forces focus update even when 1988 * podToFocus is already focused. 1989 */ 1990 focusPod: function(podToFocus, opt_force) { 1991 if (this.isFocused(podToFocus) && !opt_force) { 1992 // Calling focusPod w/o podToFocus means reset. 1993 if (!podToFocus) 1994 Oobe.clearErrors(); 1995 this.keyboardActivated_ = false; 1996 return; 1997 } 1998 1999 // Make sure that we don't focus pods that are not allowed to be focused. 2000 // TODO(nkostylev): Fix various keyboard focus related issues caused 2001 // by this approach. http://crbug.com/339042 2002 if (podToFocus && podToFocus.classList.contains('not-focusable')) { 2003 this.keyboardActivated_ = false; 2004 return; 2005 } 2006 2007 // Make sure there's only one focusPod operation happening at a time. 2008 if (this.insideFocusPod_) { 2009 this.keyboardActivated_ = false; 2010 return; 2011 } 2012 this.insideFocusPod_ = true; 2013 2014 for (var i = 0, pod; pod = this.pods[i]; ++i) { 2015 if (!this.alwaysFocusSinglePod) { 2016 pod.isActionBoxMenuActive = false; 2017 } 2018 if (pod != podToFocus) { 2019 pod.isActionBoxMenuHovered = false; 2020 pod.classList.remove('focused'); 2021 // On Desktop, the faded style is not set correctly, so we should 2022 // manually fade out non-focused pods. 2023 if (pod.user.isDesktopUser) 2024 pod.classList.add('faded'); 2025 else 2026 pod.classList.remove('faded'); 2027 pod.reset(false); 2028 } 2029 } 2030 2031 // Clear any error messages for previous pod. 2032 if (!this.isFocused(podToFocus)) 2033 Oobe.clearErrors(); 2034 2035 var hadFocus = !!this.focusedPod_; 2036 this.focusedPod_ = podToFocus; 2037 if (podToFocus) { 2038 podToFocus.classList.remove('faded'); 2039 podToFocus.classList.add('focused'); 2040 podToFocus.reset(true); // Reset and give focus. 2041 // focusPod() automatically loads wallpaper 2042 if (!podToFocus.user.isApp) 2043 chrome.send('focusPod', [podToFocus.user.username]); 2044 this.firstShown_ = false; 2045 this.lastFocusedPod_ = podToFocus; 2046 2047 if (Oobe.getInstance().virtualKeyboardShown) 2048 this.scrollFocusedPodIntoView(); 2049 } 2050 this.insideFocusPod_ = false; 2051 this.keyboardActivated_ = false; 2052 }, 2053 2054 /** 2055 * Focuses a given user pod by index or clear focus when given null. 2056 * @param {int=} podToFocus index of User pod to focus. 2057 * @param {boolean=} opt_force If true, forces focus update even when 2058 * podToFocus is already focused. 2059 */ 2060 focusPodByIndex: function(podToFocus, opt_force) { 2061 if (podToFocus < this.pods.length) 2062 this.focusPod(this.pods[podToFocus], opt_force); 2063 }, 2064 2065 /** 2066 * Resets wallpaper to the last active user's wallpaper, if any. 2067 */ 2068 loadLastWallpaper: function() { 2069 if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp) 2070 chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]); 2071 }, 2072 2073 /** 2074 * Returns the currently activated pod. 2075 * @type {UserPod} 2076 */ 2077 get activatedPod() { 2078 return this.activatedPod_; 2079 }, 2080 2081 /** 2082 * Sets currently activated pod. 2083 * @param {UserPod} pod Pod to check for focus. 2084 * @param {Event} e Event object. 2085 */ 2086 setActivatedPod: function(pod, e) { 2087 if (pod && pod.activate(e)) 2088 this.activatedPod_ = pod; 2089 }, 2090 2091 /** 2092 * The pod of the signed-in user, if any; null otherwise. 2093 * @type {?UserPod} 2094 */ 2095 get lockedPod() { 2096 for (var i = 0, pod; pod = this.pods[i]; ++i) { 2097 if (pod.user.signedIn) 2098 return pod; 2099 } 2100 return null; 2101 }, 2102 2103 /** 2104 * The pod that is preselected on user pod row show. 2105 * @type {?UserPod} 2106 */ 2107 get preselectedPod() { 2108 var lockedPod = this.lockedPod; 2109 var preselectedPod = PRESELECT_FIRST_POD ? 2110 lockedPod || this.pods[0] : lockedPod; 2111 return preselectedPod; 2112 }, 2113 2114 /** 2115 * Resets input UI. 2116 * @param {boolean} takeFocus True to take focus. 2117 */ 2118 reset: function(takeFocus) { 2119 this.disabled = false; 2120 if (this.activatedPod_) 2121 this.activatedPod_.reset(takeFocus); 2122 }, 2123 2124 /** 2125 * Restores input focus to current selected pod, if there is any. 2126 */ 2127 refocusCurrentPod: function() { 2128 if (this.focusedPod_) { 2129 this.focusedPod_.focusInput(); 2130 } 2131 }, 2132 2133 /** 2134 * Clears focused pod password field. 2135 */ 2136 clearFocusedPod: function() { 2137 if (!this.disabled && this.focusedPod_) 2138 this.focusedPod_.reset(true); 2139 }, 2140 2141 /** 2142 * Shows signin UI. 2143 * @param {string} email Email for signin UI. 2144 */ 2145 showSigninUI: function(email) { 2146 // Clear any error messages that might still be around. 2147 Oobe.clearErrors(); 2148 this.disabled = true; 2149 this.lastFocusedPod_ = this.getPodWithUsername_(email); 2150 Oobe.showSigninUI(email); 2151 }, 2152 2153 /** 2154 * Updates current image of a user. 2155 * @param {string} username User for which to update the image. 2156 */ 2157 updateUserImage: function(username) { 2158 var pod = this.getPodWithUsername_(username); 2159 if (pod) 2160 pod.updateUserImage(); 2161 }, 2162 2163 /** 2164 * Handler of click event. 2165 * @param {Event} e Click Event object. 2166 * @private 2167 */ 2168 handleClick_: function(e) { 2169 if (this.disabled) 2170 return; 2171 2172 // Clear all menus if the click is outside pod menu and its 2173 // button area. 2174 if (!findAncestorByClass(e.target, 'action-box-menu') && 2175 !findAncestorByClass(e.target, 'action-box-area')) { 2176 for (var i = 0, pod; pod = this.pods[i]; ++i) 2177 pod.isActionBoxMenuActive = false; 2178 } 2179 2180 // Clears focus if not clicked on a pod and if there's more than one pod. 2181 var pod = findAncestorByClass(e.target, 'pod'); 2182 if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) { 2183 this.focusPod(); 2184 } 2185 2186 if (pod) 2187 pod.isActionBoxMenuHovered = true; 2188 2189 // Return focus back to single pod. 2190 if (this.alwaysFocusSinglePod) { 2191 this.focusPod(this.focusedPod_, true /* force */); 2192 if (!pod) 2193 this.focusedPod_.isActionBoxMenuHovered = false; 2194 } 2195 }, 2196 2197 /** 2198 * Handler of mouse move event. 2199 * @param {Event} e Click Event object. 2200 * @private 2201 */ 2202 handleMouseMove_: function(e) { 2203 if (this.disabled) 2204 return; 2205 if (e.webkitMovementX == 0 && e.webkitMovementY == 0) 2206 return; 2207 2208 // Defocus (thus hide) action box, if it is focused on a user pod 2209 // and the pointer is not hovering over it. 2210 var pod = findAncestorByClass(e.target, 'pod'); 2211 if (document.activeElement && 2212 document.activeElement.parentNode != pod && 2213 document.activeElement.classList.contains('action-box-area')) { 2214 document.activeElement.parentNode.focus(); 2215 } 2216 2217 if (pod) 2218 pod.isActionBoxMenuHovered = true; 2219 2220 // Hide action boxes on other user pods. 2221 for (var i = 0, p; p = this.pods[i]; ++i) 2222 if (p != pod && !p.isActionBoxMenuActive) 2223 p.isActionBoxMenuHovered = false; 2224 }, 2225 2226 /** 2227 * Handles focus event. 2228 * @param {Event} e Focus Event object. 2229 * @private 2230 */ 2231 handleFocus_: function(e) { 2232 if (this.disabled) 2233 return; 2234 if (e.target.parentNode == this) { 2235 // Focus on a pod 2236 if (e.target.classList.contains('focused')) 2237 e.target.focusInput(); 2238 else 2239 this.focusPod(e.target); 2240 return; 2241 } 2242 2243 var pod = findAncestorByClass(e.target, 'pod'); 2244 if (pod && pod.parentNode == this) { 2245 // Focus on a control of a pod but not on the action area button. 2246 if (!pod.classList.contains('focused') && 2247 !e.target.classList.contains('action-box-button')) { 2248 this.focusPod(pod); 2249 e.target.focus(); 2250 } 2251 return; 2252 } 2253 2254 // Clears pod focus when we reach here. It means new focus is neither 2255 // on a pod nor on a button/input for a pod. 2256 // Do not "defocus" user pod when it is a single pod. 2257 // That means that 'focused' class will not be removed and 2258 // input field/button will always be visible. 2259 if (!this.alwaysFocusSinglePod) 2260 this.focusPod(); 2261 }, 2262 2263 /** 2264 * Handler of keydown event. 2265 * @param {Event} e KeyDown Event object. 2266 */ 2267 handleKeyDown: function(e) { 2268 if (this.disabled) 2269 return; 2270 var editing = e.target.tagName == 'INPUT' && e.target.value; 2271 switch (e.keyIdentifier) { 2272 case 'Left': 2273 if (!editing) { 2274 this.keyboardActivated_ = true; 2275 if (this.focusedPod_ && this.focusedPod_.previousElementSibling) 2276 this.focusPod(this.focusedPod_.previousElementSibling); 2277 else 2278 this.focusPod(this.lastElementChild); 2279 2280 e.stopPropagation(); 2281 } 2282 break; 2283 case 'Right': 2284 if (!editing) { 2285 this.keyboardActivated_ = true; 2286 if (this.focusedPod_ && this.focusedPod_.nextElementSibling) 2287 this.focusPod(this.focusedPod_.nextElementSibling); 2288 else 2289 this.focusPod(this.firstElementChild); 2290 2291 e.stopPropagation(); 2292 } 2293 break; 2294 case 'Enter': 2295 if (this.focusedPod_) { 2296 var targetTag = e.target.tagName; 2297 if (e.target == this.focusedPod_.passwordElement || 2298 (targetTag != 'INPUT' && 2299 targetTag != 'BUTTON' && 2300 targetTag != 'A')) { 2301 this.setActivatedPod(this.focusedPod_, e); 2302 e.stopPropagation(); 2303 } 2304 } 2305 break; 2306 case 'U+001B': // Esc 2307 if (!this.alwaysFocusSinglePod) 2308 this.focusPod(); 2309 break; 2310 } 2311 }, 2312 2313 /** 2314 * Called right after the pod row is shown. 2315 */ 2316 handleAfterShow: function() { 2317 // Without timeout changes in pods positions will be animated even though 2318 // it happened when 'flying-pods' class was disabled. 2319 setTimeout(function() { 2320 Oobe.getInstance().toggleClass('flying-pods', true); 2321 }, 0); 2322 // Force input focus for user pod on show and once transition ends. 2323 if (this.focusedPod_) { 2324 var focusedPod = this.focusedPod_; 2325 var screen = this.parentNode; 2326 var self = this; 2327 focusedPod.addEventListener('webkitTransitionEnd', function f(e) { 2328 focusedPod.removeEventListener('webkitTransitionEnd', f); 2329 focusedPod.reset(true); 2330 // Notify screen that it is ready. 2331 screen.onShow(); 2332 }); 2333 // Guard timer for 1 second -- it would conver all possible animations. 2334 ensureTransitionEndEvent(focusedPod, 1000); 2335 } 2336 }, 2337 2338 /** 2339 * Called right before the pod row is shown. 2340 */ 2341 handleBeforeShow: function() { 2342 Oobe.getInstance().toggleClass('flying-pods', false); 2343 for (var event in this.listeners_) { 2344 this.ownerDocument.addEventListener( 2345 event, this.listeners_[event][0], this.listeners_[event][1]); 2346 } 2347 $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR; 2348 2349 if (this.podPlacementPostponed_) { 2350 this.podPlacementPostponed_ = false; 2351 this.placePods_(); 2352 this.focusPod(this.preselectedPod); 2353 } 2354 }, 2355 2356 /** 2357 * Called when the element is hidden. 2358 */ 2359 handleHide: function() { 2360 for (var event in this.listeners_) { 2361 this.ownerDocument.removeEventListener( 2362 event, this.listeners_[event][0], this.listeners_[event][1]); 2363 } 2364 $('login-header-bar').buttonsTabIndex = 0; 2365 }, 2366 2367 /** 2368 * Called when a pod's user image finishes loading. 2369 */ 2370 handlePodImageLoad: function(pod) { 2371 var index = this.podsWithPendingImages_.indexOf(pod); 2372 if (index == -1) { 2373 return; 2374 } 2375 2376 this.podsWithPendingImages_.splice(index, 1); 2377 if (this.podsWithPendingImages_.length == 0) { 2378 this.classList.remove('images-loading'); 2379 } 2380 } 2381 }; 2382 2383 return { 2384 PodRow: PodRow 2385 }; 2386}); 2387