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