1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 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 * Whether to preselect the first pod automatically on login screen. 19 * @type {boolean} 20 * @const 21 */ 22 var PRESELECT_FIRST_POD = true; 23 24 /** 25 * Wallpaper load delay in milliseconds. 26 * @type {number} 27 * @const 28 */ 29 var WALLPAPER_LOAD_DELAY_MS = 500; 30 31 /** 32 * Wallpaper load delay in milliseconds. TODO(nkostylev): Tune this constant. 33 * @type {number} 34 * @const 35 */ 36 var WALLPAPER_BOOT_LOAD_DELAY_MS = 100; 37 38 /** 39 * Maximum time for which the pod row remains hidden until all user images 40 * have been loaded. 41 * @type {number} 42 * @const 43 */ 44 var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000; 45 46 /** 47 * Public session help topic identifier. 48 * @type {number} 49 * @const 50 */ 51 var HELP_TOPIC_PUBLIC_SESSION = 3041033; 52 53 /** 54 * Oauth token status. These must match UserManager::OAuthTokenStatus. 55 * @enum {number} 56 * @const 57 */ 58 var OAuthTokenStatus = { 59 UNKNOWN: 0, 60 INVALID_OLD: 1, 61 VALID_OLD: 2, 62 INVALID_NEW: 3, 63 VALID_NEW: 4 64 }; 65 66 /** 67 * Tab order for user pods. Update these when adding new controls. 68 * @enum {number} 69 * @const 70 */ 71 var UserPodTabOrder = { 72 POD_INPUT: 1, // Password input fields (and whole pods themselves). 73 HEADER_BAR: 2, // Buttons on the header bar (Shutdown, Add User). 74 ACTION_BOX: 3, // Action box buttons. 75 PAD_MENU_ITEM: 4 // User pad menu items (Remove this user). 76 }; 77 78 // Focus and tab order are organized as follows: 79 // 80 // (1) all user pods have tab index 1 so they are traversed first; 81 // (2) when a user pod is activated, its tab index is set to -1 and its 82 // main input field gets focus and tab index 1; 83 // (3) buttons on the header bar have tab index 2 so they follow user pods; 84 // (4) Action box buttons have tab index 3 and follow header bar buttons; 85 // (5) lastly, focus jumps to the Status Area and back to user pods. 86 // 87 // 'Focus' event is handled by a capture handler for the whole document 88 // and in some cases 'mousedown' event handlers are used instead of 'click' 89 // handlers where it's necessary to prevent 'focus' event from being fired. 90 91 /** 92 * Helper function to remove a class from given element. 93 * @param {!HTMLElement} el Element whose class list to change. 94 * @param {string} cl Class to remove. 95 */ 96 function removeClass(el, cl) { 97 el.classList.remove(cl); 98 } 99 100 /** 101 * Creates a user pod. 102 * @constructor 103 * @extends {HTMLDivElement} 104 */ 105 var UserPod = cr.ui.define(function() { 106 var node = $('user-pod-template').cloneNode(true); 107 node.removeAttribute('id'); 108 return node; 109 }); 110 111 /** 112 * Stops event propagation from the any user pod child element. 113 * @param {Event} e Event to handle. 114 */ 115 function stopEventPropagation(e) { 116 // Prevent default so that we don't trigger a 'focus' event. 117 e.preventDefault(); 118 e.stopPropagation(); 119 } 120 121 /** 122 * Unique salt added to user image URLs to prevent caching. Dictionary with 123 * user names as keys. 124 * @type {Object} 125 */ 126 UserPod.userImageSalt_ = {}; 127 128 UserPod.prototype = { 129 __proto__: HTMLDivElement.prototype, 130 131 /** @override */ 132 decorate: function() { 133 this.tabIndex = UserPodTabOrder.POD_INPUT; 134 this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX; 135 136 // Mousedown has to be used instead of click to be able to prevent 'focus' 137 // event later. 138 this.addEventListener('mousedown', 139 this.handleMouseDown_.bind(this)); 140 141 this.signinButtonElement.addEventListener('click', 142 this.activate.bind(this)); 143 144 this.actionBoxAreaElement.addEventListener('mousedown', 145 stopEventPropagation); 146 this.actionBoxAreaElement.addEventListener('click', 147 this.handleActionAreaButtonClick_.bind(this)); 148 this.actionBoxAreaElement.addEventListener('keydown', 149 this.handleActionAreaButtonKeyDown_.bind(this)); 150 151 this.actionBoxMenuRemoveElement.addEventListener('click', 152 this.handleRemoveCommandClick_.bind(this)); 153 this.actionBoxMenuRemoveElement.addEventListener('keydown', 154 this.handleRemoveCommandKeyDown_.bind(this)); 155 this.actionBoxMenuRemoveElement.addEventListener('blur', 156 this.handleRemoveCommandBlur_.bind(this)); 157 158 if (this.actionBoxRemoveUserWarningButtonElement) { 159 this.actionBoxRemoveUserWarningButtonElement.addEventListener( 160 'click', 161 this.handleRemoveUserConfirmationClick_.bind(this)); 162 } 163 }, 164 165 /** 166 * Initializes the pod after its properties set and added to a pod row. 167 */ 168 initialize: function() { 169 this.passwordElement.addEventListener('keydown', 170 this.parentNode.handleKeyDown.bind(this.parentNode)); 171 this.passwordElement.addEventListener('keypress', 172 this.handlePasswordKeyPress_.bind(this)); 173 174 this.imageElement.addEventListener('load', 175 this.parentNode.handlePodImageLoad.bind(this.parentNode, this)); 176 }, 177 178 /** 179 * Resets tab order for pod elements to its initial state. 180 */ 181 resetTabOrder: function() { 182 this.tabIndex = UserPodTabOrder.POD_INPUT; 183 this.mainInput.tabIndex = -1; 184 }, 185 186 /** 187 * Handles keypress event (i.e. any textual input) on password input. 188 * @param {Event} e Keypress Event object. 189 * @private 190 */ 191 handlePasswordKeyPress_: function(e) { 192 // When tabbing from the system tray a tab key press is received. Suppress 193 // this so as not to type a tab character into the password field. 194 if (e.keyCode == 9) { 195 e.preventDefault(); 196 return; 197 } 198 }, 199 200 /** 201 * Gets signed in indicator element. 202 * @type {!HTMLDivElement} 203 */ 204 get signedInIndicatorElement() { 205 return this.querySelector('.signed-in-indicator'); 206 }, 207 208 /** 209 * Gets image element. 210 * @type {!HTMLImageElement} 211 */ 212 get imageElement() { 213 return this.querySelector('.user-image'); 214 }, 215 216 /** 217 * Gets name element. 218 * @type {!HTMLDivElement} 219 */ 220 get nameElement() { 221 return this.querySelector('.name'); 222 }, 223 224 /** 225 * Gets password field. 226 * @type {!HTMLInputElement} 227 */ 228 get passwordElement() { 229 return this.querySelector('.password'); 230 }, 231 232 /** 233 * Gets Caps Lock hint image. 234 * @type {!HTMLImageElement} 235 */ 236 get capslockHintElement() { 237 return this.querySelector('.capslock-hint'); 238 }, 239 240 /** 241 * Gets user signin button. 242 * @type {!HTMLInputElement} 243 */ 244 get signinButtonElement() { 245 return this.querySelector('.signin-button'); 246 }, 247 248 /** 249 * Gets action box area. 250 * @type {!HTMLInputElement} 251 */ 252 get actionBoxAreaElement() { 253 return this.querySelector('.action-box-area'); 254 }, 255 256 /** 257 * Gets user type icon area. 258 * @type {!HTMLInputElement} 259 */ 260 get userTypeIconAreaElement() { 261 return this.querySelector('.user-type-icon-area'); 262 }, 263 264 /** 265 * Gets action box menu. 266 * @type {!HTMLInputElement} 267 */ 268 get actionBoxMenuElement() { 269 return this.querySelector('.action-box-menu'); 270 }, 271 272 /** 273 * Gets action box menu title. 274 * @type {!HTMLInputElement} 275 */ 276 get actionBoxMenuTitleElement() { 277 return this.querySelector('.action-box-menu-title'); 278 }, 279 280 /** 281 * Gets action box menu title, user name item. 282 * @type {!HTMLInputElement} 283 */ 284 get actionBoxMenuTitleNameElement() { 285 return this.querySelector('.action-box-menu-title-name'); 286 }, 287 288 /** 289 * Gets action box menu title, user email item. 290 * @type {!HTMLInputElement} 291 */ 292 get actionBoxMenuTitleEmailElement() { 293 return this.querySelector('.action-box-menu-title-email'); 294 }, 295 296 /** 297 * Gets action box menu, remove user command item. 298 * @type {!HTMLInputElement} 299 */ 300 get actionBoxMenuCommandElement() { 301 return this.querySelector('.action-box-menu-remove-command'); 302 }, 303 304 /** 305 * Gets action box menu, remove user command item div. 306 * @type {!HTMLInputElement} 307 */ 308 get actionBoxMenuRemoveElement() { 309 return this.querySelector('.action-box-menu-remove'); 310 }, 311 312 /** 313 * Gets action box menu, remove user command item div. 314 * @type {!HTMLInputElement} 315 */ 316 get actionBoxRemoveUserWarningElement() { 317 return this.querySelector('.action-box-remove-user-warning'); 318 }, 319 320 /** 321 * Gets action box menu, remove user command item div. 322 * @type {!HTMLInputElement} 323 */ 324 get actionBoxRemoveUserWarningButtonElement() { 325 return this.querySelector( 326 '.remove-warning-button'); 327 }, 328 329 /** 330 * Updates the user pod element. 331 */ 332 update: function() { 333 this.imageElement.src = 'chrome://userimage/' + this.user.username + 334 '?id=' + UserPod.userImageSalt_[this.user.username]; 335 336 this.nameElement.textContent = this.user_.displayName; 337 this.signedInIndicatorElement.hidden = !this.user_.signedIn; 338 339 var needSignin = this.needGaiaSignin; 340 this.passwordElement.hidden = needSignin; 341 this.signinButtonElement.hidden = !needSignin; 342 343 this.updateActionBoxArea(); 344 }, 345 346 updateActionBoxArea: function() { 347 this.actionBoxAreaElement.hidden = this.user_.publicAccount; 348 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove; 349 350 this.actionBoxAreaElement.setAttribute( 351 'aria-label', loadTimeData.getStringF( 352 'podMenuButtonAccessibleName', this.user_.emailAddress)); 353 this.actionBoxMenuRemoveElement.setAttribute( 354 'aria-label', loadTimeData.getString( 355 'podMenuRemoveItemAccessibleName')); 356 this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ? 357 loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) : 358 this.user_.displayName; 359 this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress; 360 this.actionBoxMenuTitleEmailElement.hidden = 361 this.user_.locallyManagedUser; 362 363 this.actionBoxMenuCommandElement.textContent = 364 loadTimeData.getString('removeUser'); 365 this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF( 366 'passwordFieldAccessibleName', this.user_.emailAddress)); 367 this.userTypeIconAreaElement.hidden = !this.user_.locallyManagedUser; 368 }, 369 370 /** 371 * The user that this pod represents. 372 * @type {!Object} 373 */ 374 user_: undefined, 375 get user() { 376 return this.user_; 377 }, 378 set user(userDict) { 379 this.user_ = userDict; 380 this.update(); 381 }, 382 383 /** 384 * Whether Gaia signin is required for this user. 385 */ 386 get needGaiaSignin() { 387 // Gaia signin is performed if the user has an invalid oauth token and is 388 // not currently signed in (i.e. not the lock screen). 389 // Locally managed users never require GAIA signin. 390 return this.user.oauthTokenStatus != OAuthTokenStatus.VALID_OLD && 391 this.user.oauthTokenStatus != OAuthTokenStatus.VALID_NEW && 392 !this.user.signedIn && !this.user.locallyManagedUser; 393 }, 394 395 /** 396 * Gets main input element. 397 * @type {(HTMLButtonElement|HTMLInputElement)} 398 */ 399 get mainInput() { 400 if (!this.signinButtonElement.hidden) 401 return this.signinButtonElement; 402 else 403 return this.passwordElement; 404 }, 405 406 /** 407 * Whether action box button is in active state. 408 * @type {boolean} 409 */ 410 get isActionBoxMenuActive() { 411 return this.actionBoxAreaElement.classList.contains('active'); 412 }, 413 set isActionBoxMenuActive(active) { 414 if (active == this.isActionBoxMenuActive) 415 return; 416 417 if (active) { 418 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove; 419 if (this.actionBoxRemoveUserWarningElement) 420 this.actionBoxRemoveUserWarningElement.hidden = true; 421 422 // Clear focus first if another pod is focused. 423 if (!this.parentNode.isFocused(this)) { 424 this.parentNode.focusPod(undefined, true); 425 this.actionBoxAreaElement.focus(); 426 } 427 this.actionBoxAreaElement.classList.add('active'); 428 } else { 429 this.actionBoxAreaElement.classList.remove('active'); 430 } 431 }, 432 433 /** 434 * Whether action box button is in hovered state. 435 * @type {boolean} 436 */ 437 get isActionBoxMenuHovered() { 438 return this.actionBoxAreaElement.classList.contains('hovered'); 439 }, 440 set isActionBoxMenuHovered(hovered) { 441 if (hovered == this.isActionBoxMenuHovered) 442 return; 443 444 if (hovered) { 445 this.actionBoxAreaElement.classList.add('hovered'); 446 } else { 447 this.actionBoxAreaElement.classList.remove('hovered'); 448 } 449 }, 450 451 /** 452 * Updates the image element of the user. 453 */ 454 updateUserImage: function() { 455 UserPod.userImageSalt_[this.user.username] = new Date().getTime(); 456 this.update(); 457 }, 458 459 /** 460 * Focuses on input element. 461 */ 462 focusInput: function() { 463 var needSignin = this.needGaiaSignin; 464 this.signinButtonElement.hidden = !needSignin; 465 this.passwordElement.hidden = needSignin; 466 467 // Move tabIndex from the whole pod to the main input. 468 this.tabIndex = -1; 469 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 470 this.mainInput.focus(); 471 }, 472 473 /** 474 * Activates the pod. 475 * @return {boolean} True if activated successfully. 476 */ 477 activate: function() { 478 if (!this.signinButtonElement.hidden) { 479 // Switch to Gaia signin. 480 this.showSigninUI(); 481 } else if (!this.passwordElement.value) { 482 return false; 483 } else { 484 Oobe.disableSigninUI(); 485 chrome.send('authenticateUser', 486 [this.user.username, this.passwordElement.value]); 487 } 488 489 return true; 490 }, 491 492 /** 493 * Shows signin UI for this user. 494 */ 495 showSigninUI: function() { 496 this.parentNode.showSigninUI(this.user.emailAddress); 497 }, 498 499 /** 500 * Resets the input field and updates the tab order of pod controls. 501 * @param {boolean} takeFocus If true, input field takes focus. 502 */ 503 reset: function(takeFocus) { 504 this.passwordElement.value = ''; 505 if (takeFocus) 506 this.focusInput(); // This will set a custom tab order. 507 else 508 this.resetTabOrder(); 509 }, 510 511 /** 512 * Handles a click event on action area button. 513 * @param {Event} e Click event. 514 */ 515 handleActionAreaButtonClick_: function(e) { 516 if (this.parentNode.disabled) 517 return; 518 this.isActionBoxMenuActive = !this.isActionBoxMenuActive; 519 }, 520 521 /** 522 * Handles a keydown event on action area button. 523 * @param {Event} e KeyDown event. 524 */ 525 handleActionAreaButtonKeyDown_: function(e) { 526 if (this.disabled) 527 return; 528 switch (e.keyIdentifier) { 529 case 'Enter': 530 case 'U+0020': // Space 531 if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive) 532 this.isActionBoxMenuActive = true; 533 e.stopPropagation(); 534 break; 535 case 'Up': 536 case 'Down': 537 if (this.isActionBoxMenuActive) { 538 this.actionBoxMenuRemoveElement.tabIndex = 539 UserPodTabOrder.PAD_MENU_ITEM; 540 this.actionBoxMenuRemoveElement.focus(); 541 } 542 e.stopPropagation(); 543 break; 544 case 'U+001B': // Esc 545 this.isActionBoxMenuActive = false; 546 e.stopPropagation(); 547 break; 548 case 'U+0009': // Tab 549 this.parentNode.focusPod(); 550 default: 551 this.isActionBoxMenuActive = false; 552 break; 553 } 554 }, 555 556 /** 557 * Handles a click event on remove user command. 558 * @param {Event} e Click event. 559 */ 560 handleRemoveCommandClick_: function(e) { 561 if (this.user.locallyManagedUser || this.user.isDesktopUser) { 562 this.showRemoveWarning_(); 563 return; 564 } 565 if (this.isActionBoxMenuActive) 566 chrome.send('removeUser', [this.user.username]); 567 }, 568 569 /** 570 * Shows remove warning for managed users. 571 */ 572 showRemoveWarning_: function() { 573 this.actionBoxMenuRemoveElement.hidden = true; 574 this.actionBoxRemoveUserWarningElement.hidden = false; 575 }, 576 577 /** 578 * Handles a click event on remove user confirmation button. 579 * @param {Event} e Click event. 580 */ 581 handleRemoveUserConfirmationClick_: function(e) { 582 if (this.isActionBoxMenuActive) 583 chrome.send('removeUser', [this.user.username]); 584 }, 585 586 /** 587 * Handles a keydown event on remove command. 588 * @param {Event} e KeyDown event. 589 */ 590 handleRemoveCommandKeyDown_: function(e) { 591 if (this.disabled) 592 return; 593 switch (e.keyIdentifier) { 594 case 'Enter': 595 chrome.send('removeUser', [this.user.username]); 596 e.stopPropagation(); 597 break; 598 case 'Up': 599 case 'Down': 600 e.stopPropagation(); 601 break; 602 case 'U+001B': // Esc 603 this.actionBoxAreaElement.focus(); 604 this.isActionBoxMenuActive = false; 605 e.stopPropagation(); 606 break; 607 default: 608 this.actionBoxAreaElement.focus(); 609 this.isActionBoxMenuActive = false; 610 break; 611 } 612 }, 613 614 /** 615 * Handles a blur event on remove command. 616 * @param {Event} e Blur event. 617 */ 618 handleRemoveCommandBlur_: function(e) { 619 if (this.disabled) 620 return; 621 this.actionBoxMenuRemoveElement.tabIndex = -1; 622 }, 623 624 /** 625 * Handles mousedown event on a user pod. 626 * @param {Event} e Mousedown event. 627 */ 628 handleMouseDown_: function(e) { 629 if (this.parentNode.disabled) 630 return; 631 632 if (!this.signinButtonElement.hidden && !this.isActionBoxMenuActive) { 633 this.showSigninUI(); 634 // Prevent default so that we don't trigger 'focus' event. 635 e.preventDefault(); 636 } 637 } 638 }; 639 640 /** 641 * Creates a public account user pod. 642 * @constructor 643 * @extends {UserPod} 644 */ 645 var PublicAccountUserPod = cr.ui.define(function() { 646 var node = UserPod(); 647 648 var extras = $('public-account-user-pod-extras-template').children; 649 for (var i = 0; i < extras.length; ++i) { 650 var el = extras[i].cloneNode(true); 651 node.appendChild(el); 652 } 653 654 return node; 655 }); 656 657 PublicAccountUserPod.prototype = { 658 __proto__: UserPod.prototype, 659 660 /** 661 * "Enter" button in expanded side pane. 662 * @type {!HTMLButtonElement} 663 */ 664 get enterButtonElement() { 665 return this.querySelector('.enter-button'); 666 }, 667 668 /** 669 * Boolean flag of whether the pod is showing the side pane. The flag 670 * controls whether 'expanded' class is added to the pod's class list and 671 * resets tab order because main input element changes when the 'expanded' 672 * state changes. 673 * @type {boolean} 674 */ 675 get expanded() { 676 return this.classList.contains('expanded'); 677 }, 678 set expanded(expanded) { 679 if (this.expanded == expanded) 680 return; 681 682 this.resetTabOrder(); 683 this.classList.toggle('expanded', expanded); 684 685 var self = this; 686 this.classList.add('animating'); 687 this.addEventListener('webkitTransitionEnd', function f(e) { 688 self.removeEventListener('webkitTransitionEnd', f); 689 self.classList.remove('animating'); 690 691 // Accessibility focus indicator does not move with the focused 692 // element. Sends a 'focus' event on the currently focused element 693 // so that accessibility focus indicator updates its location. 694 if (document.activeElement) 695 document.activeElement.dispatchEvent(new Event('focus')); 696 }); 697 }, 698 699 /** @override */ 700 get needGaiaSignin() { 701 return false; 702 }, 703 704 /** @override */ 705 get mainInput() { 706 if (this.expanded) 707 return this.enterButtonElement; 708 else 709 return this.nameElement; 710 }, 711 712 /** @override */ 713 decorate: function() { 714 UserPod.prototype.decorate.call(this); 715 716 this.classList.remove('need-password'); 717 this.classList.add('public-account'); 718 719 this.nameElement.addEventListener('keydown', (function(e) { 720 if (e.keyIdentifier == 'Enter') { 721 this.parentNode.activatedPod = this; 722 // Stop this keydown event from bubbling up to PodRow handler. 723 e.stopPropagation(); 724 // Prevent default so that we don't trigger a 'click' event on the 725 // newly focused "Enter" button. 726 e.preventDefault(); 727 } 728 }).bind(this)); 729 730 var learnMore = this.querySelector('.learn-more'); 731 learnMore.addEventListener('mousedown', stopEventPropagation); 732 learnMore.addEventListener('click', this.handleLearnMoreEvent); 733 learnMore.addEventListener('keydown', this.handleLearnMoreEvent); 734 735 learnMore = this.querySelector('.side-pane-learn-more'); 736 learnMore.addEventListener('click', this.handleLearnMoreEvent); 737 learnMore.addEventListener('keydown', this.handleLearnMoreEvent); 738 739 this.enterButtonElement.addEventListener('click', (function(e) { 740 this.enterButtonElement.disabled = true; 741 chrome.send('launchPublicAccount', [this.user.username]); 742 }).bind(this)); 743 }, 744 745 /** 746 * Updates the user pod element. 747 */ 748 update: function() { 749 UserPod.prototype.update.call(this); 750 this.querySelector('.side-pane-name').textContent = 751 this.user_.displayName; 752 this.querySelector('.info').textContent = 753 loadTimeData.getStringF('publicAccountInfoFormat', 754 this.user_.enterpriseDomain); 755 }, 756 757 /** @override */ 758 focusInput: function() { 759 // Move tabIndex from the whole pod to the main input. 760 this.tabIndex = -1; 761 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 762 this.mainInput.focus(); 763 }, 764 765 /** @override */ 766 reset: function(takeFocus) { 767 if (!takeFocus) 768 this.expanded = false; 769 this.enterButtonElement.disabled = false; 770 UserPod.prototype.reset.call(this, takeFocus); 771 }, 772 773 /** @override */ 774 activate: function() { 775 this.expanded = true; 776 this.focusInput(); 777 return true; 778 }, 779 780 /** @override */ 781 handleMouseDown_: function(e) { 782 if (this.parentNode.disabled) 783 return; 784 785 this.parentNode.focusPod(this); 786 this.parentNode.activatedPod = this; 787 // Prevent default so that we don't trigger 'focus' event. 788 e.preventDefault(); 789 }, 790 791 /** 792 * Handle mouse and keyboard events for the learn more button. 793 * Triggering the button causes information about public sessions to be 794 * shown. 795 * @param {Event} event Mouse or keyboard event. 796 */ 797 handleLearnMoreEvent: function(event) { 798 switch (event.type) { 799 // Show informaton on left click. Let any other clicks propagate. 800 case 'click': 801 if (event.button != 0) 802 return; 803 break; 804 // Show informaton when <Return> or <Space> is pressed. Let any other 805 // key presses propagate. 806 case 'keydown': 807 switch (event.keyCode) { 808 case 13: // Return. 809 case 32: // Space. 810 break; 811 default: 812 return; 813 } 814 break; 815 } 816 chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]); 817 stopEventPropagation(event); 818 }, 819 }; 820 821 /** 822 * Creates a user pod to be used only in desktop chrome. 823 * @constructor 824 * @extends {UserPod} 825 */ 826 var DesktopUserPod = cr.ui.define(function() { 827 // Don't just instantiate a UserPod(), as this will call decorate() on the 828 // parent object, and add duplicate event listeners. 829 var node = $('user-pod-template').cloneNode(true); 830 node.removeAttribute('id'); 831 return node; 832 }); 833 834 DesktopUserPod.prototype = { 835 __proto__: UserPod.prototype, 836 837 /** @override */ 838 decorate: function() { 839 UserPod.prototype.decorate.call(this); 840 }, 841 842 /** @override */ 843 focusInput: function() { 844 var isLockedUser = this.user.needsSignin; 845 this.signinButtonElement.hidden = isLockedUser; 846 this.passwordElement.hidden = !isLockedUser; 847 848 // Move tabIndex from the whole pod to the main input. 849 this.tabIndex = -1; 850 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 851 this.mainInput.focus(); 852 }, 853 854 /** @override */ 855 update: function() { 856 // TODO(noms): Use the actual profile avatar for local profiles once the 857 // new, non-pixellated avatars are available. 858 this.imageElement.src = this.user.emailAddress == '' ? 859 'chrome://theme/IDR_USER_MANAGER_DEFAULT_AVATAR' : 860 this.user.userImage; 861 this.nameElement.textContent = this.user_.displayName; 862 var isLockedUser = this.user.needsSignin; 863 this.passwordElement.hidden = !isLockedUser; 864 this.signinButtonElement.hidden = isLockedUser; 865 866 UserPod.prototype.updateActionBoxArea.call(this); 867 }, 868 869 /** @override */ 870 activate: function() { 871 Oobe.launchUser(this.user.emailAddress, this.user.displayName); 872 return true; 873 }, 874 875 /** @override */ 876 handleMouseDown_: function(e) { 877 if (this.parentNode.disabled) 878 return; 879 880 // Don't sign in until the user presses the button. Just activate the pod. 881 Oobe.clearErrors(); 882 this.parentNode.lastFocusedPod_ = 883 this.parentNode.getPodWithUsername_(this.user.emailAddress); 884 }, 885 886 /** @override */ 887 handleRemoveUserConfirmationClick_: function(e) { 888 chrome.send('removeUser', [this.user.profilePath]); 889 }, 890 }; 891 892 /** 893 * Creates a new pod row element. 894 * @constructor 895 * @extends {HTMLDivElement} 896 */ 897 var PodRow = cr.ui.define('podrow'); 898 899 PodRow.prototype = { 900 __proto__: HTMLDivElement.prototype, 901 902 // Whether this user pod row is shown for the first time. 903 firstShown_: true, 904 905 // Whether the initial wallpaper load after boot has been requested. Used 906 // only if |Oobe.getInstance().shouldLoadWallpaperOnBoot()| is true. 907 bootWallpaperLoaded_: false, 908 909 // True if inside focusPod(). 910 insideFocusPod_: false, 911 912 // True if user pod has been activated with keyboard. 913 // In case of activation with keyboard we delay wallpaper change. 914 keyboardActivated_: false, 915 916 // Focused pod. 917 focusedPod_: undefined, 918 919 // Activated pod, i.e. the pod of current login attempt. 920 activatedPod_: undefined, 921 922 // Pod that was most recently focused, if any. 923 lastFocusedPod_: undefined, 924 925 // When moving through users quickly at login screen, set a timeout to 926 // prevent loading intermediate wallpapers. 927 loadWallpaperTimeout_: null, 928 929 // Pods whose initial images haven't been loaded yet. 930 podsWithPendingImages_: [], 931 932 /** @override */ 933 decorate: function() { 934 this.style.left = 0; 935 936 // Event listeners that are installed for the time period during which 937 // the element is visible. 938 this.listeners_ = { 939 focus: [this.handleFocus_.bind(this), true], 940 click: [this.handleClick_.bind(this), false], 941 mousemove: [this.handleMouseMove_.bind(this), false], 942 keydown: [this.handleKeyDown.bind(this), false] 943 }; 944 }, 945 946 /** 947 * Returns all the pods in this pod row. 948 * @type {NodeList} 949 */ 950 get pods() { 951 return this.children; 952 }, 953 954 /** 955 * Return true if user pod row has only single user pod in it. 956 * @type {boolean} 957 */ 958 get isSinglePod() { 959 return this.children.length == 1; 960 }, 961 962 /** 963 * Returns pod with the given username (null if there is no such pod). 964 * @param {string} username Username to be matched. 965 * @return {Object} Pod with the given username. null if pod hasn't been 966 * found. 967 */ 968 getPodWithUsername_: function(username) { 969 for (var i = 0, pod; pod = this.pods[i]; ++i) { 970 if (pod.user.username == username) 971 return pod; 972 } 973 return null; 974 }, 975 976 /** 977 * True if the the pod row is disabled (handles no user interaction). 978 * @type {boolean} 979 */ 980 disabled_: false, 981 get disabled() { 982 return this.disabled_; 983 }, 984 set disabled(value) { 985 this.disabled_ = value; 986 var controls = this.querySelectorAll('button,input'); 987 for (var i = 0, control; control = controls[i]; ++i) { 988 control.disabled = value; 989 } 990 }, 991 992 /** 993 * Creates a user pod from given email. 994 * @param {string} email User's email. 995 */ 996 createUserPod: function(user) { 997 var userPod; 998 if (user.isDesktopUser) 999 userPod = new DesktopUserPod({user: user}); 1000 else if (user.publicAccount) 1001 userPod = new PublicAccountUserPod({user: user}); 1002 else 1003 userPod = new UserPod({user: user}); 1004 1005 userPod.hidden = false; 1006 return userPod; 1007 }, 1008 1009 /** 1010 * Add an existing user pod to this pod row. 1011 * @param {!Object} user User info dictionary. 1012 * @param {boolean} animated Whether to use init animation. 1013 */ 1014 addUserPod: function(user, animated) { 1015 var userPod = this.createUserPod(user); 1016 if (animated) { 1017 userPod.classList.add('init'); 1018 userPod.nameElement.classList.add('init'); 1019 } 1020 1021 this.appendChild(userPod); 1022 userPod.initialize(); 1023 }, 1024 1025 /** 1026 * Returns index of given pod or -1 if not found. 1027 * @param {UserPod} pod Pod to look up. 1028 * @private 1029 */ 1030 indexOf_: function(pod) { 1031 for (var i = 0; i < this.pods.length; ++i) { 1032 if (pod == this.pods[i]) 1033 return i; 1034 } 1035 return -1; 1036 }, 1037 1038 /** 1039 * Start first time show animation. 1040 */ 1041 startInitAnimation: function() { 1042 // Schedule init animation. 1043 for (var i = 0, pod; pod = this.pods[i]; ++i) { 1044 window.setTimeout(removeClass, 500 + i * 70, pod, 'init'); 1045 window.setTimeout(removeClass, 700 + i * 70, pod.nameElement, 'init'); 1046 } 1047 }, 1048 1049 /** 1050 * Start login success animation. 1051 */ 1052 startAuthenticatedAnimation: function() { 1053 var activated = this.indexOf_(this.activatedPod_); 1054 if (activated == -1) 1055 return; 1056 1057 for (var i = 0, pod; pod = this.pods[i]; ++i) { 1058 if (i < activated) 1059 pod.classList.add('left'); 1060 else if (i > activated) 1061 pod.classList.add('right'); 1062 else 1063 pod.classList.add('zoom'); 1064 } 1065 }, 1066 1067 /** 1068 * Populates pod row with given existing users and start init animation. 1069 * @param {array} users Array of existing user emails. 1070 * @param {boolean} animated Whether to use init animation. 1071 */ 1072 loadPods: function(users, animated) { 1073 // Clear existing pods. 1074 this.innerHTML = ''; 1075 this.focusedPod_ = undefined; 1076 this.activatedPod_ = undefined; 1077 this.lastFocusedPod_ = undefined; 1078 1079 // Populate the pod row. 1080 for (var i = 0; i < users.length; ++i) { 1081 this.addUserPod(users[i], animated); 1082 } 1083 for (var i = 0, pod; pod = this.pods[i]; ++i) { 1084 this.podsWithPendingImages_.push(pod); 1085 } 1086 // Make sure we eventually show the pod row, even if some image is stuck. 1087 setTimeout(function() { 1088 $('pod-row').classList.remove('images-loading'); 1089 }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS); 1090 1091 var columns = users.length < COLUMNS.length ? 1092 COLUMNS[users.length] : COLUMNS[COLUMNS.length - 1]; 1093 var rows = Math.floor((users.length - 1) / columns) + 1; 1094 1095 // Cancel any pending resize operation. 1096 this.removeEventListener('mouseout', this.deferredResizeListener_); 1097 1098 if (!this.columns || !this.rows) { 1099 // Set initial dimensions. 1100 this.resize_(columns, rows); 1101 } else if (columns != this.columns || rows != this.rows) { 1102 // Defer the resize until mouse cursor leaves the pod row. 1103 this.deferredResizeListener_ = function(e) { 1104 if (!findAncestorByClass(e.toElement, 'podrow')) { 1105 this.resize_(columns, rows); 1106 } 1107 }.bind(this); 1108 this.addEventListener('mouseout', this.deferredResizeListener_); 1109 } 1110 1111 this.focusPod(this.preselectedPod); 1112 }, 1113 1114 /** 1115 * Resizes the pod row and cancel any pending resize operations. 1116 * @param {number} columns Number of columns. 1117 * @param {number} rows Number of rows. 1118 * @private 1119 */ 1120 resize_: function(columns, rows) { 1121 this.removeEventListener('mouseout', this.deferredResizeListener_); 1122 this.columns = columns; 1123 this.rows = rows; 1124 if (this.parentNode == Oobe.getInstance().currentScreen) { 1125 Oobe.getInstance().updateScreenSize(this.parentNode); 1126 } 1127 }, 1128 1129 /** 1130 * Number of columns. 1131 * @type {?number} 1132 */ 1133 set columns(columns) { 1134 // Cannot use 'columns' here. 1135 this.setAttribute('ncolumns', columns); 1136 }, 1137 get columns() { 1138 return this.getAttribute('ncolumns'); 1139 }, 1140 1141 /** 1142 * Number of rows. 1143 * @type {?number} 1144 */ 1145 set rows(rows) { 1146 // Cannot use 'rows' here. 1147 this.setAttribute('nrows', rows); 1148 }, 1149 get rows() { 1150 return this.getAttribute('nrows'); 1151 }, 1152 1153 /** 1154 * Whether the pod is currently focused. 1155 * @param {UserPod} pod Pod to check for focus. 1156 * @return {boolean} Pod focus status. 1157 */ 1158 isFocused: function(pod) { 1159 return this.focusedPod_ == pod; 1160 }, 1161 1162 /** 1163 * Focuses a given user pod or clear focus when given null. 1164 * @param {UserPod=} podToFocus User pod to focus (undefined clears focus). 1165 * @param {boolean=} opt_force If true, forces focus update even when 1166 * podToFocus is already focused. 1167 */ 1168 focusPod: function(podToFocus, opt_force) { 1169 if (this.isFocused(podToFocus) && !opt_force) { 1170 this.keyboardActivated_ = false; 1171 return; 1172 } 1173 1174 // Make sure there's only one focusPod operation happening at a time. 1175 if (this.insideFocusPod_) { 1176 this.keyboardActivated_ = false; 1177 return; 1178 } 1179 this.insideFocusPod_ = true; 1180 1181 clearTimeout(this.loadWallpaperTimeout_); 1182 for (var i = 0, pod; pod = this.pods[i]; ++i) { 1183 if (!this.isSinglePod) { 1184 pod.isActionBoxMenuActive = false; 1185 } 1186 if (pod != podToFocus) { 1187 pod.isActionBoxMenuHovered = false; 1188 pod.classList.remove('focused'); 1189 pod.classList.remove('faded'); 1190 pod.reset(false); 1191 } 1192 } 1193 1194 // Clear any error messages for previous pod. 1195 if (!this.isFocused(podToFocus)) 1196 Oobe.clearErrors(); 1197 1198 var hadFocus = !!this.focusedPod_; 1199 this.focusedPod_ = podToFocus; 1200 if (podToFocus) { 1201 podToFocus.classList.remove('faded'); 1202 podToFocus.classList.add('focused'); 1203 podToFocus.reset(true); // Reset and give focus. 1204 if (hadFocus && this.keyboardActivated_) { 1205 // Delay wallpaper loading to let user tab through pods without lag. 1206 this.loadWallpaperTimeout_ = window.setTimeout( 1207 this.loadWallpaper_.bind(this), WALLPAPER_LOAD_DELAY_MS); 1208 } else if (!this.firstShown_) { 1209 // Load wallpaper immediately if there no pod was focused 1210 // previously, and it is not a boot into user pod list case. 1211 this.loadWallpaper_(); 1212 } 1213 this.firstShown_ = false; 1214 this.lastFocusedPod_ = podToFocus; 1215 } 1216 this.insideFocusPod_ = false; 1217 this.keyboardActivated_ = false; 1218 }, 1219 1220 /** 1221 * Loads wallpaper for the active user pod, if any. 1222 * @private 1223 */ 1224 loadWallpaper_: function() { 1225 if (this.focusedPod_) 1226 chrome.send('loadWallpaper', [this.focusedPod_.user.username]); 1227 }, 1228 1229 /** 1230 * Resets wallpaper to the last active user's wallpaper, if any. 1231 */ 1232 loadLastWallpaper: function() { 1233 if (this.lastFocusedPod_) 1234 chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]); 1235 }, 1236 1237 /** 1238 * Returns the currently activated pod. 1239 * @type {UserPod} 1240 */ 1241 get activatedPod() { 1242 return this.activatedPod_; 1243 }, 1244 set activatedPod(pod) { 1245 if (pod && pod.activate()) 1246 this.activatedPod_ = pod; 1247 }, 1248 1249 /** 1250 * The pod of the signed-in user, if any; null otherwise. 1251 * @type {?UserPod} 1252 */ 1253 get lockedPod() { 1254 for (var i = 0, pod; pod = this.pods[i]; ++i) { 1255 if (pod.user.signedIn) 1256 return pod; 1257 } 1258 return null; 1259 }, 1260 1261 /** 1262 * The pod that is preselected on user pod row show. 1263 * @type {?UserPod} 1264 */ 1265 get preselectedPod() { 1266 var lockedPod = this.lockedPod; 1267 var preselectedPod = PRESELECT_FIRST_POD ? 1268 lockedPod || this.pods[0] : lockedPod; 1269 return preselectedPod; 1270 }, 1271 1272 /** 1273 * Resets input UI. 1274 * @param {boolean} takeFocus True to take focus. 1275 */ 1276 reset: function(takeFocus) { 1277 this.disabled = false; 1278 if (this.activatedPod_) 1279 this.activatedPod_.reset(takeFocus); 1280 }, 1281 1282 /** 1283 * Restores input focus to current selected pod, if there is any. 1284 */ 1285 refocusCurrentPod: function() { 1286 if (this.focusedPod_) { 1287 this.focusedPod_.focusInput(); 1288 } 1289 }, 1290 1291 /** 1292 * Clears focused pod password field. 1293 */ 1294 clearFocusedPod: function() { 1295 if (!this.disabled && this.focusedPod_) 1296 this.focusedPod_.reset(true); 1297 }, 1298 1299 /** 1300 * Shows signin UI. 1301 * @param {string} email Email for signin UI. 1302 */ 1303 showSigninUI: function(email) { 1304 // Clear any error messages that might still be around. 1305 Oobe.clearErrors(); 1306 this.disabled = true; 1307 this.lastFocusedPod_ = this.getPodWithUsername_(email); 1308 Oobe.showSigninUI(email); 1309 }, 1310 1311 /** 1312 * Updates current image of a user. 1313 * @param {string} username User for which to update the image. 1314 */ 1315 updateUserImage: function(username) { 1316 var pod = this.getPodWithUsername_(username); 1317 if (pod) 1318 pod.updateUserImage(); 1319 }, 1320 1321 /** 1322 * Resets OAuth token status (invalidates it). 1323 * @param {string} username User for which to reset the status. 1324 */ 1325 resetUserOAuthTokenStatus: function(username) { 1326 var pod = this.getPodWithUsername_(username); 1327 if (pod) { 1328 pod.user.oauthTokenStatus = OAuthTokenStatus.INVALID_OLD; 1329 pod.update(); 1330 } else { 1331 console.log('Failed to update Gaia state for: ' + username); 1332 } 1333 }, 1334 1335 /** 1336 * Handler of click event. 1337 * @param {Event} e Click Event object. 1338 * @private 1339 */ 1340 handleClick_: function(e) { 1341 if (this.disabled) 1342 return; 1343 1344 // Clear all menus if the click is outside pod menu and its 1345 // button area. 1346 if (!findAncestorByClass(e.target, 'action-box-menu') && 1347 !findAncestorByClass(e.target, 'action-box-area')) { 1348 for (var i = 0, pod; pod = this.pods[i]; ++i) 1349 pod.isActionBoxMenuActive = false; 1350 } 1351 1352 // Clears focus if not clicked on a pod and if there's more than one pod. 1353 var pod = findAncestorByClass(e.target, 'pod'); 1354 if ((!pod || pod.parentNode != this) && !this.isSinglePod) { 1355 this.focusPod(); 1356 } 1357 1358 if (pod) 1359 pod.isActionBoxMenuHovered = true; 1360 1361 // Return focus back to single pod. 1362 if (this.isSinglePod) { 1363 this.focusPod(this.focusedPod_, true /* force */); 1364 if (!pod) 1365 this.focusedPod_.isActionBoxMenuHovered = false; 1366 } 1367 }, 1368 1369 /** 1370 * Handler of mouse move event. 1371 * @param {Event} e Click Event object. 1372 * @private 1373 */ 1374 handleMouseMove_: function(e) { 1375 if (this.disabled) 1376 return; 1377 if (e.webkitMovementX == 0 && e.webkitMovementY == 0) 1378 return; 1379 1380 // Defocus (thus hide) action box, if it is focused on a user pod 1381 // and the pointer is not hovering over it. 1382 var pod = findAncestorByClass(e.target, 'pod'); 1383 if (document.activeElement && 1384 document.activeElement.parentNode != pod && 1385 document.activeElement.classList.contains('action-box-area')) { 1386 document.activeElement.parentNode.focus(); 1387 } 1388 1389 if (pod) 1390 pod.isActionBoxMenuHovered = true; 1391 1392 // Hide action boxes on other user pods. 1393 for (var i = 0, p; p = this.pods[i]; ++i) 1394 if (p != pod && !p.isActionBoxMenuActive) 1395 p.isActionBoxMenuHovered = false; 1396 }, 1397 1398 /** 1399 * Handles focus event. 1400 * @param {Event} e Focus Event object. 1401 * @private 1402 */ 1403 handleFocus_: function(e) { 1404 if (this.disabled) 1405 return; 1406 if (e.target.parentNode == this) { 1407 // Focus on a pod 1408 if (e.target.classList.contains('focused')) 1409 e.target.focusInput(); 1410 else 1411 this.focusPod(e.target); 1412 return; 1413 } 1414 1415 var pod = findAncestorByClass(e.target, 'pod'); 1416 if (pod && pod.parentNode == this) { 1417 // Focus on a control of a pod but not on the action area button. 1418 if (!pod.classList.contains('focused') && 1419 !e.target.classList.contains('action-box-button')) { 1420 this.focusPod(pod); 1421 e.target.focus(); 1422 } 1423 return; 1424 } 1425 1426 // Clears pod focus when we reach here. It means new focus is neither 1427 // on a pod nor on a button/input for a pod. 1428 // Do not "defocus" user pod when it is a single pod. 1429 // That means that 'focused' class will not be removed and 1430 // input field/button will always be visible. 1431 if (!this.isSinglePod) 1432 this.focusPod(); 1433 }, 1434 1435 /** 1436 * Handler of keydown event. 1437 * @param {Event} e KeyDown Event object. 1438 */ 1439 handleKeyDown: function(e) { 1440 if (this.disabled) 1441 return; 1442 var editing = e.target.tagName == 'INPUT' && e.target.value; 1443 switch (e.keyIdentifier) { 1444 case 'Left': 1445 if (!editing) { 1446 this.keyboardActivated_ = true; 1447 if (this.focusedPod_ && this.focusedPod_.previousElementSibling) 1448 this.focusPod(this.focusedPod_.previousElementSibling); 1449 else 1450 this.focusPod(this.lastElementChild); 1451 1452 e.stopPropagation(); 1453 } 1454 break; 1455 case 'Right': 1456 if (!editing) { 1457 this.keyboardActivated_ = true; 1458 if (this.focusedPod_ && this.focusedPod_.nextElementSibling) 1459 this.focusPod(this.focusedPod_.nextElementSibling); 1460 else 1461 this.focusPod(this.firstElementChild); 1462 1463 e.stopPropagation(); 1464 } 1465 break; 1466 case 'Enter': 1467 if (this.focusedPod_) { 1468 this.activatedPod = this.focusedPod_; 1469 e.stopPropagation(); 1470 } 1471 break; 1472 case 'U+001B': // Esc 1473 if (!this.isSinglePod) 1474 this.focusPod(); 1475 break; 1476 } 1477 }, 1478 1479 /** 1480 * Called right after the pod row is shown. 1481 */ 1482 handleAfterShow: function() { 1483 // Force input focus for user pod on show and once transition ends. 1484 if (this.focusedPod_) { 1485 var focusedPod = this.focusedPod_; 1486 var screen = this.parentNode; 1487 var self = this; 1488 focusedPod.addEventListener('webkitTransitionEnd', function f(e) { 1489 if (e.target == focusedPod) { 1490 focusedPod.removeEventListener('webkitTransitionEnd', f); 1491 focusedPod.reset(true); 1492 // Notify screen that it is ready. 1493 screen.onShow(); 1494 // Boot transition: load wallpaper. 1495 if (!self.bootWallpaperLoaded_ && 1496 Oobe.getInstance().shouldLoadWallpaperOnBoot()) { 1497 self.loadWallpaperTimeout_ = window.setTimeout( 1498 self.loadWallpaper_.bind(self), WALLPAPER_BOOT_LOAD_DELAY_MS); 1499 self.bootWallpaperLoaded_ = true; 1500 } 1501 } 1502 }); 1503 } 1504 }, 1505 1506 /** 1507 * Called right before the pod row is shown. 1508 */ 1509 handleBeforeShow: function() { 1510 for (var event in this.listeners_) { 1511 this.ownerDocument.addEventListener( 1512 event, this.listeners_[event][0], this.listeners_[event][1]); 1513 } 1514 $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR; 1515 }, 1516 1517 /** 1518 * Called when the element is hidden. 1519 */ 1520 handleHide: function() { 1521 for (var event in this.listeners_) { 1522 this.ownerDocument.removeEventListener( 1523 event, this.listeners_[event][0], this.listeners_[event][1]); 1524 } 1525 $('login-header-bar').buttonsTabIndex = 0; 1526 }, 1527 1528 /** 1529 * Called when a pod's user image finishes loading. 1530 */ 1531 handlePodImageLoad: function(pod) { 1532 var index = this.podsWithPendingImages_.indexOf(pod); 1533 if (index == -1) { 1534 return; 1535 } 1536 1537 this.podsWithPendingImages_.splice(index, 1); 1538 if (this.podsWithPendingImages_.length == 0) { 1539 this.classList.remove('images-loading'); 1540 } 1541 } 1542 }; 1543 1544 return { 1545 PodRow: PodRow 1546 }; 1547}); 1548