1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * @fileoverview User pod row implementation. 7 */ 8 9cr.define('login', function() { 10 /** 11 * Number of displayed columns depending on user pod count. 12 * @type {Array.<number>} 13 * @const 14 */ 15 var COLUMNS = [0, 1, 2, 3, 4, 5, 4, 4, 4, 5, 5, 6, 6, 5, 5, 6, 6, 6, 6]; 16 17 /** 18 * Mapping between number of columns in pod-row and margin between user pods 19 * for such layout. 20 * @type {Array.<number>} 21 * @const 22 */ 23 var MARGIN_BY_COLUMNS = [undefined, 40, 40, 40, 40, 40, 12]; 24 25 /** 26 * Mapping between number of columns in the desktop pod-row and margin 27 * between user pods for such layout. 28 * @type {Array.<number>} 29 * @const 30 */ 31 var DESKTOP_MARGIN_BY_COLUMNS = [undefined, 15, 15, 15, 15, 15, 15]; 32 33 /** 34 * Maximal number of columns currently supported by pod-row. 35 * @type {number} 36 * @const 37 */ 38 var MAX_NUMBER_OF_COLUMNS = 6; 39 40 /** 41 * Maximal number of rows if sign-in banner is displayed alonside. 42 * @type {number} 43 * @const 44 */ 45 var MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER = 2; 46 47 /** 48 * Variables used for pod placement processing. Width and height should be 49 * synced with computed CSS sizes of pods. 50 */ 51 var POD_WIDTH = 180; 52 var PUBLIC_EXPANDED_BASIC_WIDTH = 500; 53 var PUBLIC_EXPANDED_ADVANCED_WIDTH = 610; 54 var CROS_POD_HEIGHT = 213; 55 var DESKTOP_POD_HEIGHT = 226; 56 var POD_ROW_PADDING = 10; 57 var DESKTOP_ROW_PADDING = 15; 58 var CUSTOM_ICON_CONTAINER_SIZE = 40; 59 60 /** 61 * Minimal padding between user pod and virtual keyboard. 62 * @type {number} 63 * @const 64 */ 65 var USER_POD_KEYBOARD_MIN_PADDING = 20; 66 67 /** 68 * Maximum time for which the pod row remains hidden until all user images 69 * have been loaded. 70 * @type {number} 71 * @const 72 */ 73 var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000; 74 75 /** 76 * Public session help topic identifier. 77 * @type {number} 78 * @const 79 */ 80 var HELP_TOPIC_PUBLIC_SESSION = 3041033; 81 82 /** 83 * Tab order for user pods. Update these when adding new controls. 84 * @enum {number} 85 * @const 86 */ 87 var UserPodTabOrder = { 88 POD_INPUT: 1, // Password input fields (and whole pods themselves). 89 POD_CUSTOM_ICON: 2, // Pod custom icon next to passwrod input field. 90 HEADER_BAR: 3, // Buttons on the header bar (Shutdown, Add User). 91 ACTION_BOX: 4, // Action box buttons. 92 PAD_MENU_ITEM: 5 // User pad menu items (Remove this user). 93 }; 94 95 /** 96 * Supported authentication types. Keep in sync with the enum in 97 * chrome/browser/signin/screenlock_bridge.h 98 * @enum {number} 99 * @const 100 */ 101 var AUTH_TYPE = { 102 OFFLINE_PASSWORD: 0, 103 ONLINE_SIGN_IN: 1, 104 NUMERIC_PIN: 2, 105 USER_CLICK: 3, 106 EXPAND_THEN_USER_CLICK: 4, 107 FORCE_OFFLINE_PASSWORD: 5 108 }; 109 110 /** 111 * Names of authentication types. 112 */ 113 var AUTH_TYPE_NAMES = { 114 0: 'offlinePassword', 115 1: 'onlineSignIn', 116 2: 'numericPin', 117 3: 'userClick', 118 4: 'expandThenUserClick', 119 5: 'forceOfflinePassword' 120 }; 121 122 // Focus and tab order are organized as follows: 123 // 124 // (1) all user pods have tab index 1 so they are traversed first; 125 // (2) when a user pod is activated, its tab index is set to -1 and its 126 // main input field gets focus and tab index 1; 127 // (3) if user pod custom icon is interactive, it has tab index 2 so it 128 // follows the input. 129 // (4) buttons on the header bar have tab index 3 so they follow the custom 130 // icon, or user pod if custom icon is not interactive; 131 // (5) Action box buttons have tab index 4 and follow header bar buttons; 132 // (6) lastly, focus jumps to the Status Area and back to user pods. 133 // 134 // 'Focus' event is handled by a capture handler for the whole document 135 // and in some cases 'mousedown' event handlers are used instead of 'click' 136 // handlers where it's necessary to prevent 'focus' event from being fired. 137 138 /** 139 * Helper function to remove a class from given element. 140 * @param {!HTMLElement} el Element whose class list to change. 141 * @param {string} cl Class to remove. 142 */ 143 function removeClass(el, cl) { 144 el.classList.remove(cl); 145 } 146 147 /** 148 * Creates a user pod. 149 * @constructor 150 * @extends {HTMLDivElement} 151 */ 152 var UserPod = cr.ui.define(function() { 153 var node = $('user-pod-template').cloneNode(true); 154 node.removeAttribute('id'); 155 return node; 156 }); 157 158 /** 159 * Stops event propagation from the any user pod child element. 160 * @param {Event} e Event to handle. 161 */ 162 function stopEventPropagation(e) { 163 // Prevent default so that we don't trigger a 'focus' event. 164 e.preventDefault(); 165 e.stopPropagation(); 166 } 167 168 /** 169 * Creates an element for custom icon shown in a user pod next to the input 170 * field. 171 * @constructor 172 * @extends {HTMLDivElement} 173 */ 174 var UserPodCustomIcon = cr.ui.define(function() { 175 var node = document.createElement('div'); 176 node.classList.add('custom-icon-container'); 177 node.hidden = true; 178 179 // Create the actual icon element and add it as a child to the container. 180 var iconNode = document.createElement('div'); 181 iconNode.classList.add('custom-icon'); 182 node.appendChild(iconNode); 183 return node; 184 }); 185 186 /** 187 * The supported user pod custom icons. 188 * {@code id} properties should be in sync with values set by C++ side. 189 * {@code class} properties are CSS classes used to set the icons' background. 190 * @const {Array.<{id: !string, class: !string}>} 191 */ 192 UserPodCustomIcon.ICONS = [ 193 {id: 'locked', class: 'custom-icon-locked'}, 194 {id: 'unlocked', class: 'custom-icon-unlocked'}, 195 {id: 'hardlocked', class: 'custom-icon-hardlocked'}, 196 {id: 'spinner', class: 'custom-icon-spinner'} 197 ]; 198 199 UserPodCustomIcon.prototype = { 200 __proto__: HTMLDivElement.prototype, 201 202 /** 203 * The id of the icon being shown. 204 * @type {string} 205 * @private 206 */ 207 iconId_: '', 208 209 /** 210 * Tooltip to be shown when the user hovers over the icon. The icon 211 * properties may be set so the tooltip is shown automatically when the icon 212 * is updated. The tooltip is shown in a bubble attached to the icon 213 * element. 214 * @type {string} 215 * @private 216 */ 217 tooltip_: '', 218 219 /** 220 * Whether the tooltip is shown and the user is hovering over the icon. 221 * @type {boolean} 222 * @private 223 */ 224 tooltipActive_: false, 225 226 /** 227 * Whether the icon has been shown as a result of |autoshow| parameter begin 228 * set rather than user hovering over the icon. 229 * If this is set, the tooltip will not be removed when the mouse leaves the 230 * icon. 231 * @type {boolean} 232 * @private 233 */ 234 tooltipAutoshown_: false, 235 236 /** 237 * A reference to the timeout for showing tooltip after mouse enters the 238 * icon. 239 * @type {?number} 240 * @private 241 */ 242 showTooltipTimeout_: null, 243 244 /** 245 * When {@code fadeOut} is called, the element gets hidden using fadeout 246 * animation. This is reference to the listener for transition end added to 247 * the icon element while it's fading out. 248 * @type {?function(Event)} 249 * @private 250 */ 251 hideTransitionListener_: null, 252 253 /** 254 * Callback for click and 'Enter' key events that gets set if the icon is 255 * interactive. 256 * @type {?function()} 257 * @private 258 */ 259 actionHandler_: null, 260 261 /** @override */ 262 decorate: function() { 263 this.iconElement.addEventListener('mouseover', 264 this.showTooltipSoon_.bind(this)); 265 this.iconElement.addEventListener('mouseout', 266 this.hideTooltip_.bind(this, false)); 267 this.iconElement.addEventListener('mousedown', 268 this.handleMouseDown_.bind(this)); 269 this.iconElement.addEventListener('click', 270 this.handleClick_.bind(this)); 271 this.iconElement.addEventListener('keydown', 272 this.handleKeyDown_.bind(this)); 273 274 // When the icon is focused using mouse, there should be no outline shown. 275 // Preventing default mousedown event accomplishes this. 276 this.iconElement.addEventListener('mousedown', function(e) { 277 e.preventDefault(); 278 }); 279 }, 280 281 /** 282 * Getter for the icon element's div. 283 * @return {HTMLDivElement} 284 */ 285 get iconElement() { 286 return this.querySelector('.custom-icon'); 287 }, 288 289 /** 290 * Updates the icon element class list to properly represent the provided 291 * icon. 292 * @param {!string} id The id of the icon that should be shown. Should be 293 * one of the ids listed in {@code UserPodCustomIcon.ICONS}. 294 */ 295 setIcon: function(id) { 296 this.iconId_ = id; 297 UserPodCustomIcon.ICONS.forEach(function(icon) { 298 this.iconElement.classList.toggle(icon.class, id == icon.id); 299 }, this); 300 }, 301 302 /** 303 * Sets the ARIA label for the icon. 304 * @param {!string} ariaLabel 305 */ 306 setAriaLabel: function(ariaLabel) { 307 this.iconElement.setAttribute('aria-label', ariaLabel); 308 }, 309 310 /** 311 * Shows the icon. 312 */ 313 show: function() { 314 this.resetHideTransitionState_(); 315 this.hidden = false; 316 }, 317 318 /** 319 * Hides the icon using a fade-out animation. 320 */ 321 fadeOut: function() { 322 // The icon is already being hidden. 323 if (this.iconElement.classList.contains('faded')) 324 return; 325 326 this.hideTooltip_(true); 327 this.iconElement.classList.add('faded'); 328 this.hideTransitionListener_ = this.hide.bind(this); 329 this.iconElement.addEventListener('webkitTransitionEnd', 330 this.hideTransitionListener_); 331 ensureTransitionEndEvent(this.iconElement, 200); 332 }, 333 334 /** 335 * Updates the icon tooltip. If {@code autoshow} parameter is set the 336 * tooltip is immediatelly shown. If tooltip text is not set, the method 337 * ensures the tooltip gets hidden. If tooltip is shown prior to this call, 338 * it remains shown, but the tooltip text is updated. 339 * @param {!{text: string, autoshow: boolean}} tooltip The tooltip 340 * parameters. 341 */ 342 setTooltip: function(tooltip) { 343 this.iconElement.classList.toggle('icon-with-tooltip', !!tooltip.text); 344 345 if (this.tooltip_ == tooltip.text && !tooltip.autoshow) 346 return; 347 this.tooltip_ = tooltip.text; 348 349 // If tooltip is already shown, just update the text. 350 if (tooltip.text && this.tooltipActive_ && !$('bubble').hidden) { 351 // If both previous and the new tooltip are supposed to be shown 352 // automatically, keep the autoshown flag. 353 var markAutoshown = this.tooltipAutoshown_ && tooltip.autoshow; 354 this.hideTooltip_(true); 355 this.showTooltip_(); 356 this.tooltipAutoshown_ = markAutoshown; 357 return; 358 } 359 360 // If autoshow flag is set, make sure the tooltip gets shown. 361 if (tooltip.text && tooltip.autoshow) { 362 this.hideTooltip_(true); 363 this.showTooltip_(); 364 this.tooltipAutoshown_ = true; 365 // Reset the tooltip active flag, which gets set in |showTooltip_|. 366 this.tooltipActive_ = false; 367 return; 368 } 369 370 this.hideTooltip_(true); 371 }, 372 373 /** 374 * Sets up icon tabIndex attribute and handler for click and 'Enter' key 375 * down events. 376 * @param {?function()} callback If icon should be interactive, the 377 * function to get called on click and 'Enter' key down events. Should 378 * be null to make the icon non interactive. 379 */ 380 setInteractive: function(callback) { 381 this.iconElement.classList.toggle('interactive-custom-icon', !!callback); 382 383 // Update tabIndex property if needed. 384 if (!!this.actionHandler_ != !!callback) { 385 if (callback) { 386 this.iconElement.setAttribute('tabIndex', 387 UserPodTabOrder.POD_CUSTOM_ICON); 388 } else { 389 this.iconElement.removeAttribute('tabIndex'); 390 } 391 } 392 393 // Set the new action handler. 394 this.actionHandler_ = callback; 395 }, 396 397 /** 398 * Hides the icon and cleans its state. 399 */ 400 hide: function() { 401 this.hideTooltip_(true); 402 this.hidden = true; 403 this.setInteractive(null); 404 this.resetHideTransitionState_(); 405 }, 406 407 /** 408 * Ensures the icon's transition state potentially set by a call to 409 * {@code fadeOut} is cleared. 410 * @private 411 */ 412 resetHideTransitionState_: function() { 413 if (this.hideTransitionListener_) { 414 this.iconElement.removeEventListener('webkitTransitionEnd', 415 this.hideTransitionListener_); 416 this.hideTransitionListener_ = null; 417 } 418 this.iconElement.classList.toggle('faded', false); 419 }, 420 421 /** 422 * Handles mouse down event in the icon element. 423 * @param {Event} e The mouse down event. 424 * @private 425 */ 426 handleMouseDown_: function(e) { 427 this.hideTooltip_(false); 428 // Stop the event propagation so in the case the click ends up on the 429 // user pod (outside the custom icon) auth is not attempted. 430 stopEventPropagation(e); 431 }, 432 433 /** 434 * Handles click event on the icon element. No-op if 435 * {@code this.actionHandler_} is not set. 436 * @param {Event} e The click event. 437 * @private 438 */ 439 handleClick_: function(e) { 440 if (!this.actionHandler_) 441 return; 442 this.actionHandler_(); 443 stopEventPropagation(e); 444 }, 445 446 /** 447 * Handles key down event on the icon element. Only 'Enter' key is handled. 448 * No-op if {@code this.actionHandler_} is not set. 449 * @param {Event} e The key down event. 450 * @private 451 */ 452 handleKeyDown_: function(e) { 453 if (!this.actionHandler_ || e.keyIdentifier != 'Enter') 454 return; 455 this.actionHandler_(e); 456 stopEventPropagation(e); 457 }, 458 459 /** 460 * Called when mouse enters the icon. It sets timeout for showing the 461 * tooltip. 462 * @private 463 */ 464 showTooltipSoon_: function() { 465 if (this.showTooltipTimeout_ || this.tooltipActive_) 466 return; 467 this.showTooltipTimeout_ = 468 setTimeout(this.showTooltip_.bind(this), 1000); 469 }, 470 471 /** 472 * Shows the current tooltip if one is set. 473 * @private 474 */ 475 showTooltip_: function() { 476 if (this.hidden || !this.tooltip_ || this.tooltipActive_) 477 return; 478 479 // If autoshown bubble got hidden, clear the autoshown flag. 480 if ($('bubble').hidden && this.tooltipAutoshown_) 481 this.tooltipAutoshown_ = false; 482 483 // Show the tooltip bubble. 484 var bubbleContent = document.createElement('div'); 485 bubbleContent.textContent = this.tooltip_; 486 487 /** @const */ var BUBBLE_OFFSET = CUSTOM_ICON_CONTAINER_SIZE / 2; 488 // TODO(tengs): Introduce a special reauth state for the account picker, 489 // instead of showing the tooltip bubble here (crbug.com/409427). 490 /** @const */ var BUBBLE_PADDING = 8 + (this.iconId_ ? 0 : 23); 491 $('bubble').showContentForElement(this, 492 cr.ui.Bubble.Attachment.RIGHT, 493 bubbleContent, 494 BUBBLE_OFFSET, 495 BUBBLE_PADDING); 496 this.ensureTooltipTimeoutCleared_(); 497 this.tooltipActive_ = true; 498 }, 499 500 /** 501 * Hides the tooltip. If the current tooltip was automatically shown, it 502 * will be hidden only if |force| is set. 503 * @param {boolean} Whether the tooltip should be hidden if it got shown 504 * because autoshow flag was set. 505 * @private 506 */ 507 hideTooltip_: function(force) { 508 this.tooltipActive_ = false; 509 this.ensureTooltipTimeoutCleared_(); 510 if (!force && this.tooltipAutoshown_) 511 return; 512 $('bubble').hideForElement(this); 513 this.tooltipAutoshown_ = false; 514 }, 515 516 /** 517 * Clears timaout for showing the tooltip if it's set. 518 * @private 519 */ 520 ensureTooltipTimeoutCleared_: function() { 521 if (this.showTooltipTimeout_) { 522 clearTimeout(this.showTooltipTimeout_); 523 this.showTooltipTimeout_ = null; 524 } 525 }, 526 }; 527 528 /** 529 * Unique salt added to user image URLs to prevent caching. Dictionary with 530 * user names as keys. 531 * @type {Object} 532 */ 533 UserPod.userImageSalt_ = {}; 534 535 UserPod.prototype = { 536 __proto__: HTMLDivElement.prototype, 537 538 /** 539 * Whether click on the pod can issue a user click auth attempt. The 540 * attempt can be issued iff the pod was focused when the click 541 * started (i.e. on mouse down event). 542 * @type {boolean} 543 * @private 544 */ 545 userClickAuthAllowed_: false, 546 547 /** @override */ 548 decorate: function() { 549 this.tabIndex = UserPodTabOrder.POD_INPUT; 550 this.actionBoxAreaElement.tabIndex = UserPodTabOrder.ACTION_BOX; 551 552 this.addEventListener('keydown', this.handlePodKeyDown_.bind(this)); 553 this.addEventListener('click', this.handleClickOnPod_.bind(this)); 554 this.addEventListener('mousedown', this.handlePodMouseDown_.bind(this)); 555 556 this.signinButtonElement.addEventListener('click', 557 this.activate.bind(this)); 558 559 this.actionBoxAreaElement.addEventListener('mousedown', 560 stopEventPropagation); 561 this.actionBoxAreaElement.addEventListener('click', 562 this.handleActionAreaButtonClick_.bind(this)); 563 this.actionBoxAreaElement.addEventListener('keydown', 564 this.handleActionAreaButtonKeyDown_.bind(this)); 565 566 this.actionBoxMenuRemoveElement.addEventListener('click', 567 this.handleRemoveCommandClick_.bind(this)); 568 this.actionBoxMenuRemoveElement.addEventListener('keydown', 569 this.handleRemoveCommandKeyDown_.bind(this)); 570 this.actionBoxMenuRemoveElement.addEventListener('blur', 571 this.handleRemoveCommandBlur_.bind(this)); 572 this.actionBoxRemoveUserWarningButtonElement.addEventListener( 573 'click', 574 this.handleRemoveUserConfirmationClick_.bind(this)); 575 this.actionBoxRemoveUserWarningButtonElement.addEventListener( 576 'keydown', 577 this.handleRemoveUserConfirmationKeyDown_.bind(this)); 578 579 var customIcon = this.customIconElement; 580 customIcon.parentNode.replaceChild(new UserPodCustomIcon(), customIcon); 581 }, 582 583 /** 584 * Initializes the pod after its properties set and added to a pod row. 585 */ 586 initialize: function() { 587 this.passwordElement.addEventListener('keydown', 588 this.parentNode.handleKeyDown.bind(this.parentNode)); 589 this.passwordElement.addEventListener('keypress', 590 this.handlePasswordKeyPress_.bind(this)); 591 592 this.imageElement.addEventListener('load', 593 this.parentNode.handlePodImageLoad.bind(this.parentNode, this)); 594 595 var initialAuthType = this.user.initialAuthType || 596 AUTH_TYPE.OFFLINE_PASSWORD; 597 this.setAuthType(initialAuthType, null); 598 599 this.userClickAuthAllowed_ = false; 600 }, 601 602 /** 603 * Resets tab order for pod elements to its initial state. 604 */ 605 resetTabOrder: function() { 606 // Note: the |mainInput| can be the pod itself. 607 this.mainInput.tabIndex = -1; 608 this.tabIndex = UserPodTabOrder.POD_INPUT; 609 }, 610 611 /** 612 * Handles keypress event (i.e. any textual input) on password input. 613 * @param {Event} e Keypress Event object. 614 * @private 615 */ 616 handlePasswordKeyPress_: function(e) { 617 // When tabbing from the system tray a tab key press is received. Suppress 618 // this so as not to type a tab character into the password field. 619 if (e.keyCode == 9) { 620 e.preventDefault(); 621 return; 622 } 623 }, 624 625 /** 626 * Top edge margin number of pixels. 627 * @type {?number} 628 */ 629 set top(top) { 630 this.style.top = cr.ui.toCssPx(top); 631 }, 632 633 /** 634 * Top edge margin number of pixels. 635 */ 636 get top() { 637 return parseInt(this.style.top); 638 }, 639 640 /** 641 * Left edge margin number of pixels. 642 * @type {?number} 643 */ 644 set left(left) { 645 this.style.left = cr.ui.toCssPx(left); 646 }, 647 648 /** 649 * Left edge margin number of pixels. 650 */ 651 get left() { 652 return parseInt(this.style.left); 653 }, 654 655 /** 656 * Height number of pixels. 657 */ 658 get height() { 659 return this.offsetHeight; 660 }, 661 662 /** 663 * Gets image element. 664 * @type {!HTMLImageElement} 665 */ 666 get imageElement() { 667 return this.querySelector('.user-image'); 668 }, 669 670 /** 671 * Gets name element. 672 * @type {!HTMLDivElement} 673 */ 674 get nameElement() { 675 return this.querySelector('.name'); 676 }, 677 678 /** 679 * Gets the container holding the password field. 680 * @type {!HTMLInputElement} 681 */ 682 get passwordEntryContainerElement() { 683 return this.querySelector('.password-entry-container'); 684 }, 685 686 /** 687 * Gets password field. 688 * @type {!HTMLInputElement} 689 */ 690 get passwordElement() { 691 return this.querySelector('.password'); 692 }, 693 694 /** 695 * Gets the password label, which is used to show a message where the 696 * password field is normally. 697 * @type {!HTMLInputElement} 698 */ 699 get passwordLabelElement() { 700 return this.querySelector('.password-label'); 701 }, 702 703 /** 704 * Gets user sign in button. 705 * @type {!HTMLButtonElement} 706 */ 707 get signinButtonElement() { 708 return this.querySelector('.signin-button'); 709 }, 710 711 /** 712 * Gets the container holding the launch app button. 713 * @type {!HTMLButtonElement} 714 */ 715 get launchAppButtonContainerElement() { 716 return this.querySelector('.launch-app-button-container'); 717 }, 718 719 /** 720 * Gets launch app button. 721 * @type {!HTMLButtonElement} 722 */ 723 get launchAppButtonElement() { 724 return this.querySelector('.launch-app-button'); 725 }, 726 727 /** 728 * Gets action box area. 729 * @type {!HTMLInputElement} 730 */ 731 get actionBoxAreaElement() { 732 return this.querySelector('.action-box-area'); 733 }, 734 735 /** 736 * Gets user type icon area. 737 * @type {!HTMLDivElement} 738 */ 739 get userTypeIconAreaElement() { 740 return this.querySelector('.user-type-icon-area'); 741 }, 742 743 /** 744 * Gets user type bubble like multi-profiles policy restriction message. 745 * @type {!HTMLDivElement} 746 */ 747 get userTypeBubbleElement() { 748 return this.querySelector('.user-type-bubble'); 749 }, 750 751 /** 752 * Gets action box menu. 753 * @type {!HTMLInputElement} 754 */ 755 get actionBoxMenu() { 756 return this.querySelector('.action-box-menu'); 757 }, 758 759 /** 760 * Gets action box menu title, user name item. 761 * @type {!HTMLInputElement} 762 */ 763 get actionBoxMenuTitleNameElement() { 764 return this.querySelector('.action-box-menu-title-name'); 765 }, 766 767 /** 768 * Gets action box menu title, user email item. 769 * @type {!HTMLInputElement} 770 */ 771 get actionBoxMenuTitleEmailElement() { 772 return this.querySelector('.action-box-menu-title-email'); 773 }, 774 775 /** 776 * Gets action box menu, remove user command item. 777 * @type {!HTMLInputElement} 778 */ 779 get actionBoxMenuCommandElement() { 780 return this.querySelector('.action-box-menu-remove-command'); 781 }, 782 783 /** 784 * Gets action box menu, remove user command item div. 785 * @type {!HTMLInputElement} 786 */ 787 get actionBoxMenuRemoveElement() { 788 return this.querySelector('.action-box-menu-remove'); 789 }, 790 791 /** 792 * Gets action box menu, remove user warning text div. 793 * @type {!HTMLInputElement} 794 */ 795 get actionBoxRemoveUserWarningTextElement() { 796 return this.querySelector('.action-box-remove-user-warning-text'); 797 }, 798 799 /** 800 * Gets action box menu, remove supervised user warning text div. 801 * @type {!HTMLInputElement} 802 */ 803 get actionBoxRemoveSupervisedUserWarningTextElement() { 804 return this.querySelector( 805 '.action-box-remove-supervised-user-warning-text'); 806 }, 807 808 /** 809 * Gets action box menu, remove user command item div. 810 * @type {!HTMLInputElement} 811 */ 812 get actionBoxRemoveUserWarningElement() { 813 return this.querySelector('.action-box-remove-user-warning'); 814 }, 815 816 /** 817 * Gets action box menu, remove user command item div. 818 * @type {!HTMLInputElement} 819 */ 820 get actionBoxRemoveUserWarningButtonElement() { 821 return this.querySelector('.remove-warning-button'); 822 }, 823 824 /** 825 * Gets the custom icon. This icon is normally hidden, but can be shown 826 * using the chrome.screenlockPrivate API. 827 * @type {!HTMLDivElement} 828 */ 829 get customIconElement() { 830 return this.querySelector('.custom-icon-container'); 831 }, 832 833 /** 834 * Updates the user pod element. 835 */ 836 update: function() { 837 this.imageElement.src = 'chrome://userimage/' + this.user.username + 838 '?id=' + UserPod.userImageSalt_[this.user.username]; 839 840 this.nameElement.textContent = this.user_.displayName; 841 this.classList.toggle('signed-in', this.user_.signedIn); 842 843 if (this.isAuthTypeUserClick) 844 this.passwordLabelElement.textContent = this.authValue; 845 846 this.updateActionBoxArea(); 847 848 this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF( 849 'passwordFieldAccessibleName', this.user_.emailAddress)); 850 851 this.customizeUserPodPerUserType(); 852 }, 853 854 updateActionBoxArea: function() { 855 if (this.user_.publicAccount || this.user_.isApp) { 856 this.actionBoxAreaElement.hidden = true; 857 return; 858 } 859 860 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove; 861 862 this.actionBoxAreaElement.setAttribute( 863 'aria-label', loadTimeData.getStringF( 864 'podMenuButtonAccessibleName', this.user_.emailAddress)); 865 this.actionBoxMenuRemoveElement.setAttribute( 866 'aria-label', loadTimeData.getString( 867 'podMenuRemoveItemAccessibleName')); 868 this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ? 869 loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) : 870 this.user_.displayName; 871 this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress; 872 this.actionBoxMenuTitleEmailElement.hidden = this.user_.supervisedUser; 873 874 this.actionBoxMenuCommandElement.textContent = 875 loadTimeData.getString('removeUser'); 876 }, 877 878 customizeUserPodPerUserType: function() { 879 if (this.user_.supervisedUser && !this.user_.isDesktopUser) { 880 this.setUserPodIconType('supervised'); 881 } else if (this.multiProfilesPolicyApplied) { 882 // Mark user pod as not focusable which in addition to the grayed out 883 // filter makes it look in disabled state. 884 this.classList.add('multiprofiles-policy-applied'); 885 this.setUserPodIconType('policy'); 886 887 if (this.user.multiProfilesPolicy == 'primary-only') 888 this.querySelector('.mp-policy-primary-only-msg').hidden = false; 889 else if (this.user.multiProfilesPolicy == 'owner-primary-only') 890 this.querySelector('.mp-owner-primary-only-msg').hidden = false; 891 else 892 this.querySelector('.mp-policy-not-allowed-msg').hidden = false; 893 } else if (this.user_.isApp) { 894 this.setUserPodIconType('app'); 895 } 896 }, 897 898 setUserPodIconType: function(userTypeClass) { 899 this.userTypeIconAreaElement.classList.add(userTypeClass); 900 this.userTypeIconAreaElement.hidden = false; 901 }, 902 903 /** 904 * The user that this pod represents. 905 * @type {!Object} 906 */ 907 user_: undefined, 908 get user() { 909 return this.user_; 910 }, 911 set user(userDict) { 912 this.user_ = userDict; 913 this.update(); 914 }, 915 916 /** 917 * Returns true if multi-profiles sign in is currently active and this 918 * user pod is restricted per policy. 919 * @type {boolean} 920 */ 921 get multiProfilesPolicyApplied() { 922 var isMultiProfilesUI = 923 (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING); 924 return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed; 925 }, 926 927 /** 928 * Gets main input element. 929 * @type {(HTMLButtonElement|HTMLInputElement)} 930 */ 931 get mainInput() { 932 if (this.isAuthTypePassword) { 933 return this.passwordElement; 934 } else if (this.isAuthTypeOnlineSignIn) { 935 return this.signinButtonElement; 936 } else if (this.isAuthTypeUserClick) { 937 return this.passwordLabelElement; 938 } 939 }, 940 941 /** 942 * Whether action box button is in active state. 943 * @type {boolean} 944 */ 945 get isActionBoxMenuActive() { 946 return this.actionBoxAreaElement.classList.contains('active'); 947 }, 948 set isActionBoxMenuActive(active) { 949 if (active == this.isActionBoxMenuActive) 950 return; 951 952 if (active) { 953 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove; 954 this.actionBoxRemoveUserWarningElement.hidden = true; 955 956 // Clear focus first if another pod is focused. 957 if (!this.parentNode.isFocused(this)) { 958 this.parentNode.focusPod(undefined, true); 959 this.actionBoxAreaElement.focus(); 960 } 961 962 // Hide user-type-bubble. 963 this.userTypeBubbleElement.classList.remove('bubble-shown'); 964 965 this.actionBoxAreaElement.classList.add('active'); 966 967 // If the user pod is on either edge of the screen, then the menu 968 // could be displayed partially ofscreen. 969 this.actionBoxMenu.classList.remove('left-edge-offset'); 970 this.actionBoxMenu.classList.remove('right-edge-offset'); 971 972 var offsetLeft = 973 cr.ui.login.DisplayManager.getOffset(this.actionBoxMenu).left; 974 var menuWidth = this.actionBoxMenu.offsetWidth; 975 if (offsetLeft < 0) 976 this.actionBoxMenu.classList.add('left-edge-offset'); 977 else if (offsetLeft + menuWidth > window.innerWidth) 978 this.actionBoxMenu.classList.add('right-edge-offset'); 979 } else { 980 this.actionBoxAreaElement.classList.remove('active'); 981 this.actionBoxAreaElement.classList.remove('menu-moved-up'); 982 this.actionBoxMenu.classList.remove('menu-moved-up'); 983 } 984 }, 985 986 /** 987 * Whether action box button is in hovered state. 988 * @type {boolean} 989 */ 990 get isActionBoxMenuHovered() { 991 return this.actionBoxAreaElement.classList.contains('hovered'); 992 }, 993 set isActionBoxMenuHovered(hovered) { 994 if (hovered == this.isActionBoxMenuHovered) 995 return; 996 997 if (hovered) { 998 this.actionBoxAreaElement.classList.add('hovered'); 999 this.classList.add('hovered'); 1000 } else { 1001 if (this.multiProfilesPolicyApplied) 1002 this.userTypeBubbleElement.classList.remove('bubble-shown'); 1003 this.actionBoxAreaElement.classList.remove('hovered'); 1004 this.classList.remove('hovered'); 1005 } 1006 }, 1007 1008 /** 1009 * Set the authentication type for the pod. 1010 * @param {number} An auth type value defined in the AUTH_TYPE enum. 1011 * @param {string} authValue The initial value used for the auth type. 1012 */ 1013 setAuthType: function(authType, authValue) { 1014 this.authType_ = authType; 1015 this.authValue_ = authValue; 1016 this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]); 1017 this.update(); 1018 this.reset(this.parentNode.isFocused(this)); 1019 }, 1020 1021 /** 1022 * The auth type of the user pod. This value is one of the enum 1023 * values in AUTH_TYPE. 1024 * @type {number} 1025 */ 1026 get authType() { 1027 return this.authType_; 1028 }, 1029 1030 /** 1031 * The initial value used for the pod's authentication type. 1032 * eg. a prepopulated password input when using password authentication. 1033 */ 1034 get authValue() { 1035 return this.authValue_; 1036 }, 1037 1038 /** 1039 * True if the the user pod uses a password to authenticate. 1040 * @type {bool} 1041 */ 1042 get isAuthTypePassword() { 1043 return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD || 1044 this.authType_ == AUTH_TYPE.FORCE_OFFLINE_PASSWORD; 1045 }, 1046 1047 /** 1048 * True if the the user pod uses a user click to authenticate. 1049 * @type {bool} 1050 */ 1051 get isAuthTypeUserClick() { 1052 return this.authType_ == AUTH_TYPE.USER_CLICK; 1053 }, 1054 1055 /** 1056 * True if the the user pod uses a online sign in to authenticate. 1057 * @type {bool} 1058 */ 1059 get isAuthTypeOnlineSignIn() { 1060 return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN; 1061 }, 1062 1063 /** 1064 * Updates the image element of the user. 1065 */ 1066 updateUserImage: function() { 1067 UserPod.userImageSalt_[this.user.username] = new Date().getTime(); 1068 this.update(); 1069 }, 1070 1071 /** 1072 * Focuses on input element. 1073 */ 1074 focusInput: function() { 1075 // Move tabIndex from the whole pod to the main input. 1076 // Note: the |mainInput| can be the pod itself. 1077 this.tabIndex = -1; 1078 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 1079 this.mainInput.focus(); 1080 }, 1081 1082 /** 1083 * Activates the pod. 1084 * @param {Event} e Event object. 1085 * @return {boolean} True if activated successfully. 1086 */ 1087 activate: function(e) { 1088 if (this.isAuthTypeOnlineSignIn) { 1089 this.showSigninUI(); 1090 } else if (this.isAuthTypeUserClick) { 1091 Oobe.disableSigninUI(); 1092 this.classList.toggle('signing-in', true); 1093 chrome.send('attemptUnlock', [this.user.username]); 1094 } else if (this.isAuthTypePassword) { 1095 if (!this.passwordElement.value) 1096 return false; 1097 Oobe.disableSigninUI(); 1098 chrome.send('authenticateUser', 1099 [this.user.username, this.passwordElement.value]); 1100 } else { 1101 console.error('Activating user pod with invalid authentication type: ' + 1102 this.authType); 1103 } 1104 1105 return true; 1106 }, 1107 1108 showSupervisedUserSigninWarning: function() { 1109 // Supervised user token has been invalidated. 1110 // Make sure that pod is focused i.e. "Sign in" button is seen. 1111 this.parentNode.focusPod(this); 1112 1113 var error = document.createElement('div'); 1114 var messageDiv = document.createElement('div'); 1115 messageDiv.className = 'error-message-bubble'; 1116 messageDiv.textContent = 1117 loadTimeData.getString('supervisedUserExpiredTokenWarning'); 1118 error.appendChild(messageDiv); 1119 1120 $('bubble').showContentForElement( 1121 this.signinButtonElement, 1122 cr.ui.Bubble.Attachment.TOP, 1123 error, 1124 this.signinButtonElement.offsetWidth / 2, 1125 4); 1126 // Move warning bubble up if it overlaps the shelf. 1127 var maxHeight = 1128 cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping($('bubble')); 1129 if (maxHeight < $('bubble').offsetHeight) { 1130 $('bubble').showContentForElement( 1131 this.signinButtonElement, 1132 cr.ui.Bubble.Attachment.BOTTOM, 1133 error, 1134 this.signinButtonElement.offsetWidth / 2, 1135 4); 1136 } 1137 }, 1138 1139 /** 1140 * Shows signin UI for this user. 1141 */ 1142 showSigninUI: function() { 1143 if (this.user.supervisedUser && !this.user.isDesktopUser) { 1144 this.showSupervisedUserSigninWarning(); 1145 } else { 1146 // Special case for multi-profiles sign in. We show users even if they 1147 // are not allowed per policy. Restrict those users from starting GAIA. 1148 if (this.multiProfilesPolicyApplied) 1149 return; 1150 1151 this.parentNode.showSigninUI(this.user.emailAddress); 1152 } 1153 }, 1154 1155 /** 1156 * Resets the input field and updates the tab order of pod controls. 1157 * @param {boolean} takeFocus If true, input field takes focus. 1158 */ 1159 reset: function(takeFocus) { 1160 this.passwordElement.value = ''; 1161 this.classList.toggle('signing-in', false); 1162 if (takeFocus) { 1163 if (!this.multiProfilesPolicyApplied) 1164 this.focusInput(); // This will set a custom tab order. 1165 } 1166 else 1167 this.resetTabOrder(); 1168 }, 1169 1170 /** 1171 * Removes a user using the correct identifier based on user type. 1172 * @param {Object} user User to be removed. 1173 */ 1174 removeUser: function(user) { 1175 chrome.send('removeUser', 1176 [user.isDesktopUser ? user.profilePath : user.username]); 1177 }, 1178 1179 /** 1180 * Handles a click event on action area button. 1181 * @param {Event} e Click event. 1182 */ 1183 handleActionAreaButtonClick_: function(e) { 1184 if (this.parentNode.disabled) 1185 return; 1186 this.isActionBoxMenuActive = !this.isActionBoxMenuActive; 1187 e.stopPropagation(); 1188 }, 1189 1190 /** 1191 * Handles a keydown event on action area button. 1192 * @param {Event} e KeyDown event. 1193 */ 1194 handleActionAreaButtonKeyDown_: function(e) { 1195 if (this.disabled) 1196 return; 1197 switch (e.keyIdentifier) { 1198 case 'Enter': 1199 case 'U+0020': // Space 1200 if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive) 1201 this.isActionBoxMenuActive = true; 1202 e.stopPropagation(); 1203 break; 1204 case 'Up': 1205 case 'Down': 1206 if (this.isActionBoxMenuActive) { 1207 this.actionBoxMenuRemoveElement.tabIndex = 1208 UserPodTabOrder.PAD_MENU_ITEM; 1209 this.actionBoxMenuRemoveElement.focus(); 1210 } 1211 e.stopPropagation(); 1212 break; 1213 case 'U+001B': // Esc 1214 this.isActionBoxMenuActive = false; 1215 e.stopPropagation(); 1216 break; 1217 case 'U+0009': // Tab 1218 if (!this.parentNode.alwaysFocusSinglePod) 1219 this.parentNode.focusPod(); 1220 default: 1221 this.isActionBoxMenuActive = false; 1222 break; 1223 } 1224 }, 1225 1226 /** 1227 * Handles a click event on remove user command. 1228 * @param {Event} e Click event. 1229 */ 1230 handleRemoveCommandClick_: function(e) { 1231 if (this.user.supervisedUser || this.user.isDesktopUser) { 1232 this.showRemoveWarning_(); 1233 return; 1234 } 1235 if (this.isActionBoxMenuActive) 1236 chrome.send('removeUser', [this.user.username]); 1237 }, 1238 1239 /** 1240 * Shows remove user warning. Used for supervised users on CrOS, and for all 1241 * users on desktop. 1242 */ 1243 showRemoveWarning_: function() { 1244 this.actionBoxMenuRemoveElement.hidden = true; 1245 this.actionBoxRemoveUserWarningElement.hidden = false; 1246 this.actionBoxRemoveUserWarningButtonElement.focus(); 1247 1248 // Move up the menu if it overlaps shelf. 1249 var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping( 1250 this.actionBoxMenu); 1251 var actualHeight = parseInt( 1252 window.getComputedStyle(this.actionBoxMenu).height); 1253 if (maxHeight < actualHeight) { 1254 this.actionBoxMenu.classList.add('menu-moved-up'); 1255 this.actionBoxAreaElement.classList.add('menu-moved-up'); 1256 } 1257 }, 1258 1259 /** 1260 * Handles a click event on remove user confirmation button. 1261 * @param {Event} e Click event. 1262 */ 1263 handleRemoveUserConfirmationClick_: function(e) { 1264 if (this.isActionBoxMenuActive) { 1265 this.isActionBoxMenuActive = false; 1266 this.removeUser(this.user); 1267 e.stopPropagation(); 1268 } 1269 }, 1270 1271 /** 1272 * Handles a keydown event on remove user confirmation button. 1273 * @param {Event} e KeyDown event. 1274 */ 1275 handleRemoveUserConfirmationKeyDown_: function(e) { 1276 if (!this.isActionBoxMenuActive) 1277 return; 1278 1279 // Only handle pressing 'Enter' or 'Space', and let all other events 1280 // bubble to the action box menu. 1281 if (e.keyIdentifier == 'Enter' || e.keyIdentifier == 'U+0020') { 1282 this.isActionBoxMenuActive = false; 1283 this.removeUser(this.user); 1284 e.stopPropagation(); 1285 // Prevent default so that we don't trigger a 'click' event. 1286 e.preventDefault(); 1287 } 1288 }, 1289 1290 /** 1291 * Handles a keydown event on remove command. 1292 * @param {Event} e KeyDown event. 1293 */ 1294 handleRemoveCommandKeyDown_: function(e) { 1295 if (this.disabled) 1296 return; 1297 switch (e.keyIdentifier) { 1298 case 'Enter': 1299 if (this.user.supervisedUser || this.user.isDesktopUser) { 1300 // Prevent default so that we don't trigger a 'click' event on the 1301 // remove button that will be focused. 1302 e.preventDefault(); 1303 this.showRemoveWarning_(); 1304 } else { 1305 this.removeUser(this.user); 1306 } 1307 e.stopPropagation(); 1308 break; 1309 case 'Up': 1310 case 'Down': 1311 e.stopPropagation(); 1312 break; 1313 case 'U+001B': // Esc 1314 this.actionBoxAreaElement.focus(); 1315 this.isActionBoxMenuActive = false; 1316 e.stopPropagation(); 1317 break; 1318 default: 1319 this.actionBoxAreaElement.focus(); 1320 this.isActionBoxMenuActive = false; 1321 break; 1322 } 1323 }, 1324 1325 /** 1326 * Handles a blur event on remove command. 1327 * @param {Event} e Blur event. 1328 */ 1329 handleRemoveCommandBlur_: function(e) { 1330 if (this.disabled) 1331 return; 1332 this.actionBoxMenuRemoveElement.tabIndex = -1; 1333 }, 1334 1335 /** 1336 * Handles mouse down event. It sets whether the user click auth will be 1337 * allowed on the next mouse click event. The auth is allowed iff the pod 1338 * was focused on the mouse down event starting the click. 1339 * @param {Event} e The mouse down event. 1340 */ 1341 handlePodMouseDown_: function(e) { 1342 this.userClickAuthAllowed_ = this.parentNode.isFocused(this); 1343 }, 1344 1345 /** 1346 * Handles click event on a user pod. 1347 * @param {Event} e Click event. 1348 */ 1349 handleClickOnPod_: function(e) { 1350 if (this.parentNode.disabled) 1351 return; 1352 1353 if (!this.isActionBoxMenuActive) { 1354 if (this.isAuthTypeOnlineSignIn) { 1355 this.showSigninUI(); 1356 } else if (this.isAuthTypeUserClick && this.userClickAuthAllowed_) { 1357 // Note that this.userClickAuthAllowed_ is set in mouse down event 1358 // handler. 1359 this.parentNode.setActivatedPod(this); 1360 } 1361 1362 if (this.multiProfilesPolicyApplied) 1363 this.userTypeBubbleElement.classList.add('bubble-shown'); 1364 1365 // Prevent default so that we don't trigger 'focus' event. 1366 e.preventDefault(); 1367 } 1368 }, 1369 1370 /** 1371 * Handles keydown event for a user pod. 1372 * @param {Event} e Key event. 1373 */ 1374 handlePodKeyDown_: function(e) { 1375 if (!this.isAuthTypeUserClick || this.disabled) 1376 return; 1377 switch (e.keyIdentifier) { 1378 case 'Enter': 1379 case 'U+0020': // Space 1380 if (this.parentNode.isFocused(this)) 1381 this.parentNode.setActivatedPod(this); 1382 break; 1383 } 1384 } 1385 }; 1386 1387 /** 1388 * Creates a public account user pod. 1389 * @constructor 1390 * @extends {UserPod} 1391 */ 1392 var PublicAccountUserPod = cr.ui.define(function() { 1393 var node = UserPod(); 1394 1395 var extras = $('public-account-user-pod-extras-template').children; 1396 for (var i = 0; i < extras.length; ++i) { 1397 var el = extras[i].cloneNode(true); 1398 node.appendChild(el); 1399 } 1400 1401 return node; 1402 }); 1403 1404 PublicAccountUserPod.prototype = { 1405 __proto__: UserPod.prototype, 1406 1407 /** 1408 * "Enter" button in expanded side pane. 1409 * @type {!HTMLButtonElement} 1410 */ 1411 get enterButtonElement() { 1412 return this.querySelector('.enter-button'); 1413 }, 1414 1415 /** 1416 * Boolean flag of whether the pod is showing the side pane. The flag 1417 * controls whether 'expanded' class is added to the pod's class list and 1418 * resets tab order because main input element changes when the 'expanded' 1419 * state changes. 1420 * @type {boolean} 1421 */ 1422 get expanded() { 1423 return this.classList.contains('expanded'); 1424 }, 1425 1426 set expanded(expanded) { 1427 if (this.expanded == expanded) 1428 return; 1429 1430 this.resetTabOrder(); 1431 this.classList.toggle('expanded', expanded); 1432 if (expanded) { 1433 // Show the advanced expanded pod directly if there are at least two 1434 // recommended locales. This will be the case in multilingual 1435 // environments where users are likely to want to choose among locales. 1436 if (this.querySelector('.language-select').multipleRecommendedLocales) 1437 this.classList.add('advanced'); 1438 this.usualLeft = this.left; 1439 this.makeSpaceForExpandedPod_(); 1440 } else if (typeof(this.usualLeft) != 'undefined') { 1441 this.left = this.usualLeft; 1442 } 1443 1444 var self = this; 1445 this.classList.add('animating'); 1446 this.addEventListener('webkitTransitionEnd', function f(e) { 1447 self.removeEventListener('webkitTransitionEnd', f); 1448 self.classList.remove('animating'); 1449 1450 // Accessibility focus indicator does not move with the focused 1451 // element. Sends a 'focus' event on the currently focused element 1452 // so that accessibility focus indicator updates its location. 1453 if (document.activeElement) 1454 document.activeElement.dispatchEvent(new Event('focus')); 1455 }); 1456 // Guard timer set to animation duration + 20ms. 1457 ensureTransitionEndEvent(this, 200); 1458 }, 1459 1460 get advanced() { 1461 return this.classList.contains('advanced'); 1462 }, 1463 1464 /** @override */ 1465 get mainInput() { 1466 if (this.expanded) 1467 return this.enterButtonElement; 1468 else 1469 return this.nameElement; 1470 }, 1471 1472 /** @override */ 1473 decorate: function() { 1474 UserPod.prototype.decorate.call(this); 1475 1476 this.classList.add('public-account'); 1477 1478 this.nameElement.addEventListener('keydown', (function(e) { 1479 if (e.keyIdentifier == 'Enter') { 1480 this.parentNode.setActivatedPod(this, e); 1481 // Stop this keydown event from bubbling up to PodRow handler. 1482 e.stopPropagation(); 1483 // Prevent default so that we don't trigger a 'click' event on the 1484 // newly focused "Enter" button. 1485 e.preventDefault(); 1486 } 1487 }).bind(this)); 1488 1489 var learnMore = this.querySelector('.learn-more'); 1490 learnMore.addEventListener('mousedown', stopEventPropagation); 1491 learnMore.addEventListener('click', this.handleLearnMoreEvent); 1492 learnMore.addEventListener('keydown', this.handleLearnMoreEvent); 1493 1494 learnMore = this.querySelector('.expanded-pane-learn-more'); 1495 learnMore.addEventListener('click', this.handleLearnMoreEvent); 1496 learnMore.addEventListener('keydown', this.handleLearnMoreEvent); 1497 1498 var languageSelect = this.querySelector('.language-select'); 1499 languageSelect.tabIndex = UserPodTabOrder.POD_INPUT; 1500 languageSelect.manuallyChanged = false; 1501 languageSelect.addEventListener( 1502 'change', 1503 function() { 1504 languageSelect.manuallyChanged = true; 1505 this.getPublicSessionKeyboardLayouts_(); 1506 }.bind(this)); 1507 1508 var keyboardSelect = this.querySelector('.keyboard-select'); 1509 keyboardSelect.tabIndex = UserPodTabOrder.POD_INPUT; 1510 keyboardSelect.loadedLocale = null; 1511 1512 var languageAndInput = this.querySelector('.language-and-input'); 1513 languageAndInput.tabIndex = UserPodTabOrder.POD_INPUT; 1514 languageAndInput.addEventListener('click', 1515 this.transitionToAdvanced_.bind(this)); 1516 1517 this.enterButtonElement.addEventListener('click', (function(e) { 1518 this.enterButtonElement.disabled = true; 1519 var locale = this.querySelector('.language-select').value; 1520 var keyboardSelect = this.querySelector('.keyboard-select'); 1521 // The contents of |keyboardSelect| is updated asynchronously. If its 1522 // locale does not match |locale|, it has not updated yet and the 1523 // currently selected keyboard layout may not be applicable to |locale|. 1524 // Do not return any keyboard layout in this case and let the backend 1525 // choose a suitable layout. 1526 var keyboardLayout = 1527 keyboardSelect.loadedLocale == locale ? keyboardSelect.value : ''; 1528 chrome.send('launchPublicSession', 1529 [this.user.username, locale, keyboardLayout]); 1530 }).bind(this)); 1531 }, 1532 1533 /** @override **/ 1534 initialize: function() { 1535 UserPod.prototype.initialize.call(this); 1536 1537 id = this.user.username + '-keyboard'; 1538 this.querySelector('.keyboard-select-label').htmlFor = id; 1539 this.querySelector('.keyboard-select').setAttribute('id', id); 1540 1541 var id = this.user.username + '-language'; 1542 this.querySelector('.language-select-label').htmlFor = id; 1543 var languageSelect = this.querySelector('.language-select'); 1544 languageSelect.setAttribute('id', id); 1545 this.populateLanguageSelect(this.user.initialLocales, 1546 this.user.initialLocale, 1547 this.user.initialMultipleRecommendedLocales); 1548 }, 1549 1550 /** @override **/ 1551 update: function() { 1552 UserPod.prototype.update.call(this); 1553 this.querySelector('.expanded-pane-name').textContent = 1554 this.user_.displayName; 1555 this.querySelector('.info').textContent = 1556 loadTimeData.getStringF('publicAccountInfoFormat', 1557 this.user_.enterpriseDomain); 1558 }, 1559 1560 /** @override */ 1561 focusInput: function() { 1562 // Move tabIndex from the whole pod to the main input. 1563 this.tabIndex = -1; 1564 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 1565 this.mainInput.focus(); 1566 }, 1567 1568 /** @override */ 1569 reset: function(takeFocus) { 1570 if (!takeFocus) 1571 this.expanded = false; 1572 this.enterButtonElement.disabled = false; 1573 UserPod.prototype.reset.call(this, takeFocus); 1574 }, 1575 1576 /** @override */ 1577 activate: function(e) { 1578 if (!this.expanded) { 1579 this.expanded = true; 1580 this.focusInput(); 1581 } 1582 return true; 1583 }, 1584 1585 /** @override */ 1586 handleClickOnPod_: function(e) { 1587 if (this.parentNode.disabled) 1588 return; 1589 1590 this.parentNode.focusPod(this); 1591 this.parentNode.setActivatedPod(this, e); 1592 // Prevent default so that we don't trigger 'focus' event. 1593 e.preventDefault(); 1594 }, 1595 1596 /** 1597 * Updates the display name shown on the pod. 1598 * @param {string} displayName The new display name 1599 */ 1600 setDisplayName: function(displayName) { 1601 this.user_.displayName = displayName; 1602 this.update(); 1603 }, 1604 1605 /** 1606 * Handle mouse and keyboard events for the learn more button. Triggering 1607 * the button causes information about public sessions to be shown. 1608 * @param {Event} event Mouse or keyboard event. 1609 */ 1610 handleLearnMoreEvent: function(event) { 1611 switch (event.type) { 1612 // Show informaton on left click. Let any other clicks propagate. 1613 case 'click': 1614 if (event.button != 0) 1615 return; 1616 break; 1617 // Show informaton when <Return> or <Space> is pressed. Let any other 1618 // key presses propagate. 1619 case 'keydown': 1620 switch (event.keyCode) { 1621 case 13: // Return. 1622 case 32: // Space. 1623 break; 1624 default: 1625 return; 1626 } 1627 break; 1628 } 1629 chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]); 1630 stopEventPropagation(event); 1631 }, 1632 1633 makeSpaceForExpandedPod_: function() { 1634 var width = this.classList.contains('advanced') ? 1635 PUBLIC_EXPANDED_ADVANCED_WIDTH : PUBLIC_EXPANDED_BASIC_WIDTH; 1636 var isDesktopUserManager = Oobe.getInstance().displayType == 1637 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 1638 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : 1639 POD_ROW_PADDING; 1640 if (this.left + width > $('pod-row').offsetWidth - rowPadding) 1641 this.left = $('pod-row').offsetWidth - rowPadding - width; 1642 }, 1643 1644 /** 1645 * Transition the expanded pod from the basic to the advanced view. 1646 */ 1647 transitionToAdvanced_: function() { 1648 var pod = this; 1649 var languageAndInputSection = 1650 this.querySelector('.language-and-input-section'); 1651 this.classList.add('transitioning-to-advanced'); 1652 setTimeout(function() { 1653 pod.classList.add('advanced'); 1654 pod.makeSpaceForExpandedPod_(); 1655 languageAndInputSection.addEventListener('webkitTransitionEnd', 1656 function observer() { 1657 languageAndInputSection.removeEventListener('webkitTransitionEnd', 1658 observer); 1659 pod.classList.remove('transitioning-to-advanced'); 1660 pod.querySelector('.language-select').focus(); 1661 }); 1662 // Guard timer set to animation duration + 20ms. 1663 ensureTransitionEndEvent(languageAndInputSection, 380); 1664 }, 0); 1665 }, 1666 1667 /** 1668 * Retrieves the list of keyboard layouts available for the currently 1669 * selected locale. 1670 */ 1671 getPublicSessionKeyboardLayouts_: function() { 1672 var selectedLocale = this.querySelector('.language-select').value; 1673 if (selectedLocale == 1674 this.querySelector('.keyboard-select').loadedLocale) { 1675 // If the list of keyboard layouts was loaded for the currently selected 1676 // locale, it is already up to date. 1677 return; 1678 } 1679 chrome.send('getPublicSessionKeyboardLayouts', 1680 [this.user.username, selectedLocale]); 1681 }, 1682 1683 /** 1684 * Populates the keyboard layout "select" element with a list of layouts. 1685 * @param {string} locale The locale to which this list of keyboard layouts 1686 * applies 1687 * @param {!Object} list List of available keyboard layouts 1688 */ 1689 populateKeyboardSelect: function(locale, list) { 1690 if (locale != this.querySelector('.language-select').value) { 1691 // The selected locale has changed and the list of keyboard layouts is 1692 // not applicable. This method will be called again when a list of 1693 // keyboard layouts applicable to the selected locale is retrieved. 1694 return; 1695 } 1696 1697 var keyboardSelect = this.querySelector('.keyboard-select'); 1698 keyboardSelect.loadedLocale = locale; 1699 keyboardSelect.innerHTML = ''; 1700 for (var i = 0; i < list.length; ++i) { 1701 var item = list[i]; 1702 keyboardSelect.appendChild( 1703 new Option(item.title, item.value, item.selected, item.selected)); 1704 } 1705 }, 1706 1707 /** 1708 * Populates the language "select" element with a list of locales. 1709 * @param {!Object} locales The list of available locales 1710 * @param {string} defaultLocale The locale to select by default 1711 * @param {boolean} multipleRecommendedLocales Whether |locales| contains 1712 * two or more recommended locales 1713 */ 1714 populateLanguageSelect: function(locales, 1715 defaultLocale, 1716 multipleRecommendedLocales) { 1717 var languageSelect = this.querySelector('.language-select'); 1718 // If the user manually selected a locale, do not change the selection. 1719 // Otherwise, select the new |defaultLocale|. 1720 var selected = 1721 languageSelect.manuallyChanged ? languageSelect.value : defaultLocale; 1722 languageSelect.innerHTML = ''; 1723 var group = languageSelect; 1724 for (var i = 0; i < locales.length; ++i) { 1725 var item = locales[i]; 1726 if (item.optionGroupName) { 1727 group = document.createElement('optgroup'); 1728 group.label = item.optionGroupName; 1729 languageSelect.appendChild(group); 1730 } else { 1731 group.appendChild(new Option(item.title, 1732 item.value, 1733 item.value == selected, 1734 item.value == selected)); 1735 } 1736 } 1737 languageSelect.multipleRecommendedLocales = multipleRecommendedLocales; 1738 1739 // Retrieve a list of keyboard layouts applicable to the locale that is 1740 // now selected. 1741 this.getPublicSessionKeyboardLayouts_(); 1742 } 1743 }; 1744 1745 /** 1746 * Creates a user pod to be used only in desktop chrome. 1747 * @constructor 1748 * @extends {UserPod} 1749 */ 1750 var DesktopUserPod = cr.ui.define(function() { 1751 // Don't just instantiate a UserPod(), as this will call decorate() on the 1752 // parent object, and add duplicate event listeners. 1753 var node = $('user-pod-template').cloneNode(true); 1754 node.removeAttribute('id'); 1755 return node; 1756 }); 1757 1758 DesktopUserPod.prototype = { 1759 __proto__: UserPod.prototype, 1760 1761 /** @override */ 1762 get mainInput() { 1763 if (this.user.needsSignin) 1764 return this.passwordElement; 1765 else 1766 return this.nameElement; 1767 }, 1768 1769 /** @override */ 1770 update: function() { 1771 this.imageElement.src = this.user.userImage; 1772 this.nameElement.textContent = this.user.displayName; 1773 1774 var isLockedUser = this.user.needsSignin; 1775 var isSupervisedUser = this.user.supervisedUser; 1776 this.classList.toggle('locked', isLockedUser); 1777 this.classList.toggle('supervised-user', isSupervisedUser); 1778 1779 if (this.isAuthTypeUserClick) 1780 this.passwordLabelElement.textContent = this.authValue; 1781 1782 this.actionBoxRemoveUserWarningTextElement.hidden = isSupervisedUser; 1783 this.actionBoxRemoveSupervisedUserWarningTextElement.hidden = 1784 !isSupervisedUser; 1785 1786 UserPod.prototype.updateActionBoxArea.call(this); 1787 }, 1788 1789 /** @override */ 1790 focusInput: function() { 1791 // Move tabIndex from the whole pod to the main input. 1792 this.tabIndex = -1; 1793 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 1794 this.mainInput.focus(); 1795 }, 1796 1797 /** @override */ 1798 activate: function(e) { 1799 if (!this.user.needsSignin) { 1800 Oobe.launchUser(this.user.emailAddress, this.user.displayName); 1801 } else if (!this.passwordElement.value) { 1802 return false; 1803 } else { 1804 chrome.send('authenticatedLaunchUser', 1805 [this.user.emailAddress, 1806 this.user.displayName, 1807 this.passwordElement.value]); 1808 } 1809 this.passwordElement.value = ''; 1810 return true; 1811 }, 1812 1813 /** @override */ 1814 handleClickOnPod_: function(e) { 1815 if (this.parentNode.disabled) 1816 return; 1817 1818 Oobe.clearErrors(); 1819 this.parentNode.lastFocusedPod_ = this; 1820 1821 // If this is an unlocked pod, then open a browser window. Otherwise 1822 // just activate the pod and show the password field. 1823 if (!this.user.needsSignin && !this.isActionBoxMenuActive) 1824 this.activate(e); 1825 1826 if (this.isAuthTypeUserClick) 1827 chrome.send('attemptUnlock', [this.user.emailAddress]); 1828 }, 1829 }; 1830 1831 /** 1832 * Creates a user pod that represents kiosk app. 1833 * @constructor 1834 * @extends {UserPod} 1835 */ 1836 var KioskAppPod = cr.ui.define(function() { 1837 var node = UserPod(); 1838 return node; 1839 }); 1840 1841 KioskAppPod.prototype = { 1842 __proto__: UserPod.prototype, 1843 1844 /** @override */ 1845 decorate: function() { 1846 UserPod.prototype.decorate.call(this); 1847 this.launchAppButtonElement.addEventListener('click', 1848 this.activate.bind(this)); 1849 }, 1850 1851 /** @override */ 1852 update: function() { 1853 this.imageElement.src = this.user.iconUrl; 1854 this.imageElement.alt = this.user.label; 1855 this.imageElement.title = this.user.label; 1856 this.passwordEntryContainerElement.hidden = true; 1857 this.launchAppButtonContainerElement.hidden = false; 1858 this.nameElement.textContent = this.user.label; 1859 1860 UserPod.prototype.updateActionBoxArea.call(this); 1861 UserPod.prototype.customizeUserPodPerUserType.call(this); 1862 }, 1863 1864 /** @override */ 1865 get mainInput() { 1866 return this.launchAppButtonElement; 1867 }, 1868 1869 /** @override */ 1870 focusInput: function() { 1871 // Move tabIndex from the whole pod to the main input. 1872 this.tabIndex = -1; 1873 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT; 1874 this.mainInput.focus(); 1875 }, 1876 1877 /** @override */ 1878 get forceOnlineSignin() { 1879 return false; 1880 }, 1881 1882 /** @override */ 1883 activate: function(e) { 1884 var diagnosticMode = e && e.ctrlKey; 1885 this.launchApp_(this.user, diagnosticMode); 1886 return true; 1887 }, 1888 1889 /** @override */ 1890 handleClickOnPod_: function(e) { 1891 if (this.parentNode.disabled) 1892 return; 1893 1894 Oobe.clearErrors(); 1895 this.parentNode.lastFocusedPod_ = this; 1896 this.activate(e); 1897 }, 1898 1899 /** 1900 * Launch the app. If |diagnosticMode| is true, ask user to confirm. 1901 * @param {Object} app App data. 1902 * @param {boolean} diagnosticMode Whether to run the app in diagnostic 1903 * mode. 1904 */ 1905 launchApp_: function(app, diagnosticMode) { 1906 if (!diagnosticMode) { 1907 chrome.send('launchKioskApp', [app.id, false]); 1908 return; 1909 } 1910 1911 var oobe = $('oobe'); 1912 if (!oobe.confirmDiagnosticMode_) { 1913 oobe.confirmDiagnosticMode_ = 1914 new cr.ui.dialogs.ConfirmDialog(document.body); 1915 oobe.confirmDiagnosticMode_.setOkLabel( 1916 loadTimeData.getString('confirmKioskAppDiagnosticModeYes')); 1917 oobe.confirmDiagnosticMode_.setCancelLabel( 1918 loadTimeData.getString('confirmKioskAppDiagnosticModeNo')); 1919 } 1920 1921 oobe.confirmDiagnosticMode_.show( 1922 loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat', 1923 app.label), 1924 function() { 1925 chrome.send('launchKioskApp', [app.id, true]); 1926 }); 1927 }, 1928 }; 1929 1930 /** 1931 * Creates a new pod row element. 1932 * @constructor 1933 * @extends {HTMLDivElement} 1934 */ 1935 var PodRow = cr.ui.define('podrow'); 1936 1937 PodRow.prototype = { 1938 __proto__: HTMLDivElement.prototype, 1939 1940 // Whether this user pod row is shown for the first time. 1941 firstShown_: true, 1942 1943 // True if inside focusPod(). 1944 insideFocusPod_: false, 1945 1946 // Focused pod. 1947 focusedPod_: undefined, 1948 1949 // Activated pod, i.e. the pod of current login attempt. 1950 activatedPod_: undefined, 1951 1952 // Pod that was most recently focused, if any. 1953 lastFocusedPod_: undefined, 1954 1955 // Pods whose initial images haven't been loaded yet. 1956 podsWithPendingImages_: [], 1957 1958 // Whether pod placement has been postponed. 1959 podPlacementPostponed_: false, 1960 1961 // Standard user pod height/width. 1962 userPodHeight_: 0, 1963 userPodWidth_: 0, 1964 1965 // Array of apps that are shown in addition to other user pods. 1966 apps_: [], 1967 1968 // True to show app pods along with user pods. 1969 shouldShowApps_: true, 1970 1971 // Array of users that are shown (public/supervised/regular). 1972 users_: [], 1973 1974 // If we're disabling single pod autofocus for Touch View. 1975 touchViewSinglePodExperimentOn_: true, 1976 1977 1978 /** @override */ 1979 decorate: function() { 1980 // Event listeners that are installed for the time period during which 1981 // the element is visible. 1982 this.listeners_ = { 1983 focus: [this.handleFocus_.bind(this), true /* useCapture */], 1984 click: [this.handleClick_.bind(this), true], 1985 mousemove: [this.handleMouseMove_.bind(this), false], 1986 keydown: [this.handleKeyDown.bind(this), false] 1987 }; 1988 1989 var isDesktopUserManager = Oobe.getInstance().displayType == 1990 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 1991 this.userPodHeight_ = isDesktopUserManager ? DESKTOP_POD_HEIGHT : 1992 CROS_POD_HEIGHT; 1993 // Same for Chrome OS and desktop. 1994 this.userPodWidth_ = POD_WIDTH; 1995 }, 1996 1997 /** 1998 * Returns all the pods in this pod row. 1999 * @type {NodeList} 2000 */ 2001 get pods() { 2002 return Array.prototype.slice.call(this.children); 2003 }, 2004 2005 /** 2006 * Return true if user pod row has only single user pod in it, which should 2007 * always be focused except desktop and touch view modes. 2008 * @type {boolean} 2009 */ 2010 get alwaysFocusSinglePod() { 2011 var isDesktopUserManager = Oobe.getInstance().displayType == 2012 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 2013 2014 return (isDesktopUserManager || 2015 (this.touchViewSinglePodExperimentOn_ && 2016 this.touchViewEnabled_)) ? 2017 false : this.children.length == 1; 2018 }, 2019 2020 /** 2021 * Returns pod with the given app id. 2022 * @param {!string} app_id Application id to be matched. 2023 * @return {Object} Pod with the given app id. null if pod hasn't been 2024 * found. 2025 */ 2026 getPodWithAppId_: function(app_id) { 2027 for (var i = 0, pod; pod = this.pods[i]; ++i) { 2028 if (pod.user.isApp && pod.user.id == app_id) 2029 return pod; 2030 } 2031 return null; 2032 }, 2033 2034 /** 2035 * Returns pod with the given username (null if there is no such pod). 2036 * @param {string} username Username to be matched. 2037 * @return {Object} Pod with the given username. null if pod hasn't been 2038 * found. 2039 */ 2040 getPodWithUsername_: function(username) { 2041 for (var i = 0, pod; pod = this.pods[i]; ++i) { 2042 if (pod.user.username == username) 2043 return pod; 2044 } 2045 return null; 2046 }, 2047 2048 /** 2049 * True if the the pod row is disabled (handles no user interaction). 2050 * @type {boolean} 2051 */ 2052 disabled_: false, 2053 get disabled() { 2054 return this.disabled_; 2055 }, 2056 set disabled(value) { 2057 this.disabled_ = value; 2058 var controls = this.querySelectorAll('button,input'); 2059 for (var i = 0, control; control = controls[i]; ++i) { 2060 control.disabled = value; 2061 } 2062 }, 2063 2064 /** 2065 * Creates a user pod from given email. 2066 * @param {!Object} user User info dictionary. 2067 */ 2068 createUserPod: function(user) { 2069 var userPod; 2070 if (user.isDesktopUser) 2071 userPod = new DesktopUserPod({user: user}); 2072 else if (user.publicAccount) 2073 userPod = new PublicAccountUserPod({user: user}); 2074 else if (user.isApp) 2075 userPod = new KioskAppPod({user: user}); 2076 else 2077 userPod = new UserPod({user: user}); 2078 2079 userPod.hidden = false; 2080 return userPod; 2081 }, 2082 2083 /** 2084 * Add an existing user pod to this pod row. 2085 * @param {!Object} user User info dictionary. 2086 */ 2087 addUserPod: function(user) { 2088 var userPod = this.createUserPod(user); 2089 this.appendChild(userPod); 2090 userPod.initialize(); 2091 }, 2092 2093 /** 2094 * Runs app with a given id from the list of loaded apps. 2095 * @param {!string} app_id of an app to run. 2096 * @param {boolean=} opt_diagnostic_mode Whether to run the app in 2097 * diagnostic mode. Default is false. 2098 */ 2099 findAndRunAppForTesting: function(app_id, opt_diagnostic_mode) { 2100 var app = this.getPodWithAppId_(app_id); 2101 if (app) { 2102 var activationEvent = cr.doc.createEvent('MouseEvents'); 2103 var ctrlKey = opt_diagnostic_mode; 2104 activationEvent.initMouseEvent('click', true, true, null, 2105 0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null); 2106 app.dispatchEvent(activationEvent); 2107 } 2108 }, 2109 2110 /** 2111 * Removes user pod from pod row. 2112 * @param {string} email User's email. 2113 */ 2114 removeUserPod: function(username) { 2115 var podToRemove = this.getPodWithUsername_(username); 2116 if (podToRemove == null) { 2117 console.warn('Attempt to remove not existing pod for ' + username + 2118 '.'); 2119 return; 2120 } 2121 this.removeChild(podToRemove); 2122 if (this.pods.length > 0) 2123 this.placePods_(); 2124 }, 2125 2126 /** 2127 * Returns index of given pod or -1 if not found. 2128 * @param {UserPod} pod Pod to look up. 2129 * @private 2130 */ 2131 indexOf_: function(pod) { 2132 for (var i = 0; i < this.pods.length; ++i) { 2133 if (pod == this.pods[i]) 2134 return i; 2135 } 2136 return -1; 2137 }, 2138 2139 /** 2140 * Populates pod row with given existing users and start init animation. 2141 * @param {array} users Array of existing user emails. 2142 */ 2143 loadPods: function(users) { 2144 this.users_ = users; 2145 2146 this.rebuildPods(); 2147 }, 2148 2149 /** 2150 * Scrolls focused user pod into view. 2151 */ 2152 scrollFocusedPodIntoView: function() { 2153 var pod = this.focusedPod_; 2154 if (!pod) 2155 return; 2156 2157 // First check whether focused pod is already fully visible. 2158 var visibleArea = $('scroll-container'); 2159 var scrollTop = visibleArea.scrollTop; 2160 var clientHeight = visibleArea.clientHeight; 2161 var podTop = $('oobe').offsetTop + pod.offsetTop; 2162 var padding = USER_POD_KEYBOARD_MIN_PADDING; 2163 if (podTop + pod.height + padding <= scrollTop + clientHeight && 2164 podTop - padding >= scrollTop) { 2165 return; 2166 } 2167 2168 // Scroll so that user pod is as centered as possible. 2169 visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2; 2170 }, 2171 2172 /** 2173 * Rebuilds pod row using users_ and apps_ that were previously set or 2174 * updated. 2175 */ 2176 rebuildPods: function() { 2177 var emptyPodRow = this.pods.length == 0; 2178 2179 // Clear existing pods. 2180 this.innerHTML = ''; 2181 this.focusedPod_ = undefined; 2182 this.activatedPod_ = undefined; 2183 this.lastFocusedPod_ = undefined; 2184 2185 // Switch off animation 2186 Oobe.getInstance().toggleClass('flying-pods', false); 2187 2188 // Populate the pod row. 2189 for (var i = 0; i < this.users_.length; ++i) 2190 this.addUserPod(this.users_[i]); 2191 2192 for (var i = 0, pod; pod = this.pods[i]; ++i) 2193 this.podsWithPendingImages_.push(pod); 2194 2195 // TODO(nkostylev): Edge case handling when kiosk apps are not fitting. 2196 if (this.shouldShowApps_) { 2197 for (var i = 0; i < this.apps_.length; ++i) 2198 this.addUserPod(this.apps_[i]); 2199 } 2200 2201 // Make sure we eventually show the pod row, even if some image is stuck. 2202 setTimeout(function() { 2203 $('pod-row').classList.remove('images-loading'); 2204 }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS); 2205 2206 var isAccountPicker = $('login-header-bar').signinUIState == 2207 SIGNIN_UI_STATE.ACCOUNT_PICKER; 2208 2209 // Immediately recalculate pods layout only when current UI is account 2210 // picker. Otherwise postpone it. 2211 if (isAccountPicker) { 2212 this.placePods_(); 2213 this.maybePreselectPod(); 2214 2215 // Without timeout changes in pods positions will be animated even 2216 // though it happened when 'flying-pods' class was disabled. 2217 setTimeout(function() { 2218 Oobe.getInstance().toggleClass('flying-pods', true); 2219 }, 0); 2220 } else { 2221 this.podPlacementPostponed_ = true; 2222 2223 // Update [Cancel] button state. 2224 if ($('login-header-bar').signinUIState == 2225 SIGNIN_UI_STATE.GAIA_SIGNIN && 2226 emptyPodRow && 2227 this.pods.length > 0) { 2228 login.GaiaSigninScreen.updateCancelButtonState(); 2229 } 2230 } 2231 }, 2232 2233 /** 2234 * Adds given apps to the pod row. 2235 * @param {array} apps Array of apps. 2236 */ 2237 setApps: function(apps) { 2238 this.apps_ = apps; 2239 this.rebuildPods(); 2240 chrome.send('kioskAppsLoaded'); 2241 2242 // Check whether there's a pending kiosk app error. 2243 window.setTimeout(function() { 2244 chrome.send('checkKioskAppLaunchError'); 2245 }, 500); 2246 }, 2247 2248 /** 2249 * Sets whether should show app pods. 2250 * @param {boolean} shouldShowApps Whether app pods should be shown. 2251 */ 2252 setShouldShowApps: function(shouldShowApps) { 2253 if (this.shouldShowApps_ == shouldShowApps) 2254 return; 2255 2256 this.shouldShowApps_ = shouldShowApps; 2257 this.rebuildPods(); 2258 }, 2259 2260 /** 2261 * Shows a custom icon on a user pod besides the input field. 2262 * @param {string} username Username of pod to add button 2263 * @param {!{id: !string, 2264 * hardlockOnClick: boolean, 2265 * ariaLabel: string | undefined, 2266 * tooltip: ({text: string, autoshow: boolean} | undefined)}} icon 2267 * The icon parameters. 2268 */ 2269 showUserPodCustomIcon: function(username, icon) { 2270 var pod = this.getPodWithUsername_(username); 2271 if (pod == null) { 2272 console.error('Unable to show user pod button for ' + username + 2273 ': user pod not found.'); 2274 return; 2275 } 2276 2277 if (!icon.id && !icon.tooltip) 2278 return; 2279 2280 if (icon.id) 2281 pod.customIconElement.setIcon(icon.id); 2282 2283 if (icon.hardlockOnClick) { 2284 pod.customIconElement.setInteractive( 2285 this.hardlockUserPod_.bind(this, username)); 2286 } else { 2287 pod.customIconElement.setInteractive(null); 2288 } 2289 2290 var ariaLabel = icon.ariaLabel || (icon.tooltip && icon.tooltip.text); 2291 if (ariaLabel) 2292 pod.customIconElement.setAriaLabel(ariaLabel); 2293 else 2294 console.warn('No ARIA label for user pod custom icon.'); 2295 2296 pod.customIconElement.show(); 2297 2298 // This has to be called after |show| in case the tooltip should be shown 2299 // immediatelly. 2300 pod.customIconElement.setTooltip( 2301 icon.tooltip || {text: '', autoshow: false}); 2302 }, 2303 2304 /** 2305 * Hard-locks user pod for the user. If user pod is hard-locked, it can be 2306 * only unlocked using password, and the authentication type cannot be 2307 * changed. 2308 * @param {!string} username The user's username. 2309 * @private 2310 */ 2311 hardlockUserPod_: function(username) { 2312 chrome.send('hardlockPod', [username]); 2313 }, 2314 2315 /** 2316 * Hides the custom icon in the user pod added by showUserPodCustomIcon(). 2317 * @param {string} username Username of pod to remove button 2318 */ 2319 hideUserPodCustomIcon: function(username) { 2320 var pod = this.getPodWithUsername_(username); 2321 if (pod == null) { 2322 console.error('Unable to hide user pod button for ' + username + 2323 ': user pod not found.'); 2324 return; 2325 } 2326 2327 // TODO(tengs): Allow option for a fading transition. 2328 pod.customIconElement.hide(); 2329 }, 2330 2331 /** 2332 * Sets the authentication type used to authenticate the user. 2333 * @param {string} username Username of selected user 2334 * @param {number} authType Authentication type, must be one of the 2335 * values listed in AUTH_TYPE enum. 2336 * @param {string} value The initial value to use for authentication. 2337 */ 2338 setAuthType: function(username, authType, value) { 2339 var pod = this.getPodWithUsername_(username); 2340 if (pod == null) { 2341 console.error('Unable to set auth type for ' + username + 2342 ': user pod not found.'); 2343 return; 2344 } 2345 pod.setAuthType(authType, value); 2346 }, 2347 2348 /** 2349 * Sets the state of touch view mode. 2350 * @param {boolean} isTouchViewEnabled true if the mode is on. 2351 */ 2352 setTouchViewState: function(isTouchViewEnabled) { 2353 this.touchViewEnabled_ = isTouchViewEnabled; 2354 }, 2355 2356 /** 2357 * Updates the display name shown on a public session pod. 2358 * @param {string} userID The user ID of the public session 2359 * @param {string} displayName The new display name 2360 */ 2361 setPublicSessionDisplayName: function(userID, displayName) { 2362 var pod = this.getPodWithUsername_(userID); 2363 if (pod != null) 2364 pod.setDisplayName(displayName); 2365 }, 2366 2367 /** 2368 * Updates the list of locales available for a public session. 2369 * @param {string} userID The user ID of the public session 2370 * @param {!Object} locales The list of available locales 2371 * @param {string} defaultLocale The locale to select by default 2372 * @param {boolean} multipleRecommendedLocales Whether |locales| contains 2373 * two or more recommended locales 2374 */ 2375 setPublicSessionLocales: function(userID, 2376 locales, 2377 defaultLocale, 2378 multipleRecommendedLocales) { 2379 var pod = this.getPodWithUsername_(userID); 2380 if (pod != null) { 2381 pod.populateLanguageSelect(locales, 2382 defaultLocale, 2383 multipleRecommendedLocales); 2384 } 2385 }, 2386 2387 /** 2388 * Updates the list of available keyboard layouts for a public session pod. 2389 * @param {string} userID The user ID of the public session 2390 * @param {string} locale The locale to which this list of keyboard layouts 2391 * applies 2392 * @param {!Object} list List of available keyboard layouts 2393 */ 2394 setPublicSessionKeyboardLayouts: function(userID, locale, list) { 2395 var pod = this.getPodWithUsername_(userID); 2396 if (pod != null) 2397 pod.populateKeyboardSelect(locale, list); 2398 }, 2399 2400 /** 2401 * Called when window was resized. 2402 */ 2403 onWindowResize: function() { 2404 var layout = this.calculateLayout_(); 2405 if (layout.columns != this.columns || layout.rows != this.rows) 2406 this.placePods_(); 2407 2408 if (Oobe.getInstance().virtualKeyboardShown) 2409 this.scrollFocusedPodIntoView(); 2410 }, 2411 2412 /** 2413 * Returns width of podrow having |columns| number of columns. 2414 * @private 2415 */ 2416 columnsToWidth_: function(columns) { 2417 var isDesktopUserManager = Oobe.getInstance().displayType == 2418 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 2419 var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] : 2420 MARGIN_BY_COLUMNS[columns]; 2421 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : 2422 POD_ROW_PADDING; 2423 return 2 * rowPadding + columns * this.userPodWidth_ + 2424 (columns - 1) * margin; 2425 }, 2426 2427 /** 2428 * Returns height of podrow having |rows| number of rows. 2429 * @private 2430 */ 2431 rowsToHeight_: function(rows) { 2432 var isDesktopUserManager = Oobe.getInstance().displayType == 2433 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 2434 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : 2435 POD_ROW_PADDING; 2436 return 2 * rowPadding + rows * this.userPodHeight_; 2437 }, 2438 2439 /** 2440 * Calculates number of columns and rows that podrow should have in order to 2441 * hold as much its pods as possible for current screen size. Also it tries 2442 * to choose layout that looks good. 2443 * @return {{columns: number, rows: number}} 2444 */ 2445 calculateLayout_: function() { 2446 var preferredColumns = this.pods.length < COLUMNS.length ? 2447 COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1]; 2448 var maxWidth = Oobe.getInstance().clientAreaSize.width; 2449 var columns = preferredColumns; 2450 while (maxWidth < this.columnsToWidth_(columns) && columns > 1) 2451 --columns; 2452 var rows = Math.floor((this.pods.length - 1) / columns) + 1; 2453 if (getComputedStyle( 2454 $('signin-banner'), null).getPropertyValue('display') != 'none') { 2455 rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER); 2456 } 2457 var maxHeigth = Oobe.getInstance().clientAreaSize.height; 2458 while (maxHeigth < this.rowsToHeight_(rows) && rows > 1) 2459 --rows; 2460 // One more iteration if it's not enough cells to place all pods. 2461 while (maxWidth >= this.columnsToWidth_(columns + 1) && 2462 columns * rows < this.pods.length && 2463 columns < MAX_NUMBER_OF_COLUMNS) { 2464 ++columns; 2465 } 2466 return {columns: columns, rows: rows}; 2467 }, 2468 2469 /** 2470 * Places pods onto their positions onto pod grid. 2471 * @private 2472 */ 2473 placePods_: function() { 2474 var layout = this.calculateLayout_(); 2475 var columns = this.columns = layout.columns; 2476 var rows = this.rows = layout.rows; 2477 var maxPodsNumber = columns * rows; 2478 var isDesktopUserManager = Oobe.getInstance().displayType == 2479 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 2480 var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] : 2481 MARGIN_BY_COLUMNS[columns]; 2482 this.parentNode.setPreferredSize( 2483 this.columnsToWidth_(columns), this.rowsToHeight_(rows)); 2484 var height = this.userPodHeight_; 2485 var width = this.userPodWidth_; 2486 this.pods.forEach(function(pod, index) { 2487 if (index >= maxPodsNumber) { 2488 pod.hidden = true; 2489 return; 2490 } 2491 pod.hidden = false; 2492 if (pod.offsetHeight != height) { 2493 console.error('Pod offsetHeight (' + pod.offsetHeight + 2494 ') and POD_HEIGHT (' + height + ') are not equal.'); 2495 } 2496 if (pod.offsetWidth != width) { 2497 console.error('Pod offsetWidth (' + pod.offsetWidth + 2498 ') and POD_WIDTH (' + width + ') are not equal.'); 2499 } 2500 var column = index % columns; 2501 var row = Math.floor(index / columns); 2502 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING : 2503 POD_ROW_PADDING; 2504 pod.left = rowPadding + column * (width + margin); 2505 2506 // On desktop, we want the rows to always be equally spaced. 2507 pod.top = isDesktopUserManager ? row * (height + rowPadding) : 2508 row * height + rowPadding; 2509 }); 2510 Oobe.getInstance().updateScreenSize(this.parentNode); 2511 }, 2512 2513 /** 2514 * Number of columns. 2515 * @type {?number} 2516 */ 2517 set columns(columns) { 2518 // Cannot use 'columns' here. 2519 this.setAttribute('ncolumns', columns); 2520 }, 2521 get columns() { 2522 return parseInt(this.getAttribute('ncolumns')); 2523 }, 2524 2525 /** 2526 * Number of rows. 2527 * @type {?number} 2528 */ 2529 set rows(rows) { 2530 // Cannot use 'rows' here. 2531 this.setAttribute('nrows', rows); 2532 }, 2533 get rows() { 2534 return parseInt(this.getAttribute('nrows')); 2535 }, 2536 2537 /** 2538 * Whether the pod is currently focused. 2539 * @param {UserPod} pod Pod to check for focus. 2540 * @return {boolean} Pod focus status. 2541 */ 2542 isFocused: function(pod) { 2543 return this.focusedPod_ == pod; 2544 }, 2545 2546 /** 2547 * Focuses a given user pod or clear focus when given null. 2548 * @param {UserPod=} podToFocus User pod to focus (undefined clears focus). 2549 * @param {boolean=} opt_force If true, forces focus update even when 2550 * podToFocus is already focused. 2551 */ 2552 focusPod: function(podToFocus, opt_force) { 2553 if (this.isFocused(podToFocus) && !opt_force) { 2554 // Calling focusPod w/o podToFocus means reset. 2555 if (!podToFocus) 2556 Oobe.clearErrors(); 2557 this.keyboardActivated_ = false; 2558 return; 2559 } 2560 2561 // Make sure there's only one focusPod operation happening at a time. 2562 if (this.insideFocusPod_) { 2563 this.keyboardActivated_ = false; 2564 return; 2565 } 2566 this.insideFocusPod_ = true; 2567 2568 for (var i = 0, pod; pod = this.pods[i]; ++i) { 2569 if (!this.alwaysFocusSinglePod) { 2570 pod.isActionBoxMenuActive = false; 2571 } 2572 if (pod != podToFocus) { 2573 pod.isActionBoxMenuHovered = false; 2574 pod.classList.remove('focused'); 2575 // On Desktop, the faded style is not set correctly, so we should 2576 // manually fade out non-focused pods if there is a focused pod. 2577 if (pod.user.isDesktopUser && podToFocus) 2578 pod.classList.add('faded'); 2579 else 2580 pod.classList.remove('faded'); 2581 pod.reset(false); 2582 } 2583 } 2584 2585 // Clear any error messages for previous pod. 2586 if (!this.isFocused(podToFocus)) 2587 Oobe.clearErrors(); 2588 2589 var hadFocus = !!this.focusedPod_; 2590 this.focusedPod_ = podToFocus; 2591 if (podToFocus) { 2592 podToFocus.classList.remove('faded'); 2593 podToFocus.classList.add('focused'); 2594 if (!podToFocus.multiProfilesPolicyApplied) 2595 podToFocus.reset(true); // Reset and give focus. 2596 else { 2597 podToFocus.userTypeBubbleElement.classList.add('bubble-shown'); 2598 podToFocus.focus(); 2599 } 2600 2601 // focusPod() automatically loads wallpaper 2602 if (!podToFocus.user.isApp) 2603 chrome.send('focusPod', [podToFocus.user.username]); 2604 this.firstShown_ = false; 2605 this.lastFocusedPod_ = podToFocus; 2606 2607 if (Oobe.getInstance().virtualKeyboardShown) 2608 this.scrollFocusedPodIntoView(); 2609 } 2610 this.insideFocusPod_ = false; 2611 this.keyboardActivated_ = false; 2612 }, 2613 2614 /** 2615 * Resets wallpaper to the last active user's wallpaper, if any. 2616 */ 2617 loadLastWallpaper: function() { 2618 if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp) 2619 chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]); 2620 }, 2621 2622 /** 2623 * Returns the currently activated pod. 2624 * @type {UserPod} 2625 */ 2626 get activatedPod() { 2627 return this.activatedPod_; 2628 }, 2629 2630 /** 2631 * Sets currently activated pod. 2632 * @param {UserPod} pod Pod to check for focus. 2633 * @param {Event} e Event object. 2634 */ 2635 setActivatedPod: function(pod, e) { 2636 if (pod && pod.activate(e)) 2637 this.activatedPod_ = pod; 2638 }, 2639 2640 /** 2641 * The pod of the signed-in user, if any; null otherwise. 2642 * @type {?UserPod} 2643 */ 2644 get lockedPod() { 2645 for (var i = 0, pod; pod = this.pods[i]; ++i) { 2646 if (pod.user.signedIn) 2647 return pod; 2648 } 2649 return null; 2650 }, 2651 2652 /** 2653 * The pod that is preselected on user pod row show. 2654 * @type {?UserPod} 2655 */ 2656 get preselectedPod() { 2657 var isDesktopUserManager = Oobe.getInstance().displayType == 2658 DISPLAY_TYPE.DESKTOP_USER_MANAGER; 2659 if (isDesktopUserManager) { 2660 // On desktop, don't pre-select a pod if it's the only one. 2661 if (this.pods.length == 1) 2662 return null; 2663 2664 // The desktop User Manager can send the index of a pod that should be 2665 // initially focused in url hash. 2666 var podIndex = parseInt(window.location.hash.substr(1)); 2667 if (isNaN(podIndex) || podIndex >= this.pods.length) 2668 return null; 2669 return this.pods[podIndex]; 2670 } 2671 2672 var lockedPod = this.lockedPod; 2673 if (lockedPod) 2674 return lockedPod; 2675 for (var i = 0, pod; pod = this.pods[i]; ++i) { 2676 if (!pod.multiProfilesPolicyApplied) { 2677 return pod; 2678 } 2679 } 2680 return this.pods[0]; 2681 }, 2682 2683 /** 2684 * Resets input UI. 2685 * @param {boolean} takeFocus True to take focus. 2686 */ 2687 reset: function(takeFocus) { 2688 this.disabled = false; 2689 if (this.activatedPod_) 2690 this.activatedPod_.reset(takeFocus); 2691 }, 2692 2693 /** 2694 * Restores input focus to current selected pod, if there is any. 2695 */ 2696 refocusCurrentPod: function() { 2697 if (this.focusedPod_ && !this.focusedPod_.multiProfilesPolicyApplied) { 2698 this.focusedPod_.focusInput(); 2699 } 2700 }, 2701 2702 /** 2703 * Clears focused pod password field. 2704 */ 2705 clearFocusedPod: function() { 2706 if (!this.disabled && this.focusedPod_) 2707 this.focusedPod_.reset(true); 2708 }, 2709 2710 /** 2711 * Shows signin UI. 2712 * @param {string} email Email for signin UI. 2713 */ 2714 showSigninUI: function(email) { 2715 // Clear any error messages that might still be around. 2716 Oobe.clearErrors(); 2717 this.disabled = true; 2718 this.lastFocusedPod_ = this.getPodWithUsername_(email); 2719 Oobe.showSigninUI(email); 2720 }, 2721 2722 /** 2723 * Updates current image of a user. 2724 * @param {string} username User for which to update the image. 2725 */ 2726 updateUserImage: function(username) { 2727 var pod = this.getPodWithUsername_(username); 2728 if (pod) 2729 pod.updateUserImage(); 2730 }, 2731 2732 /** 2733 * Handler of click event. 2734 * @param {Event} e Click Event object. 2735 * @private 2736 */ 2737 handleClick_: function(e) { 2738 if (this.disabled) 2739 return; 2740 2741 // Clear all menus if the click is outside pod menu and its 2742 // button area. 2743 if (!findAncestorByClass(e.target, 'action-box-menu') && 2744 !findAncestorByClass(e.target, 'action-box-area')) { 2745 for (var i = 0, pod; pod = this.pods[i]; ++i) 2746 pod.isActionBoxMenuActive = false; 2747 } 2748 2749 // Clears focus if not clicked on a pod and if there's more than one pod. 2750 var pod = findAncestorByClass(e.target, 'pod'); 2751 if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) { 2752 this.focusPod(); 2753 } 2754 2755 if (pod) 2756 pod.isActionBoxMenuHovered = true; 2757 2758 // Return focus back to single pod. 2759 if (this.alwaysFocusSinglePod && !pod) { 2760 this.focusPod(this.focusedPod_, true /* force */); 2761 this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown'); 2762 this.focusedPod_.isActionBoxMenuHovered = false; 2763 } 2764 }, 2765 2766 /** 2767 * Handler of mouse move event. 2768 * @param {Event} e Click Event object. 2769 * @private 2770 */ 2771 handleMouseMove_: function(e) { 2772 if (this.disabled) 2773 return; 2774 if (e.webkitMovementX == 0 && e.webkitMovementY == 0) 2775 return; 2776 2777 // Defocus (thus hide) action box, if it is focused on a user pod 2778 // and the pointer is not hovering over it. 2779 var pod = findAncestorByClass(e.target, 'pod'); 2780 if (document.activeElement && 2781 document.activeElement.parentNode != pod && 2782 document.activeElement.classList.contains('action-box-area')) { 2783 document.activeElement.parentNode.focus(); 2784 } 2785 2786 if (pod) 2787 pod.isActionBoxMenuHovered = true; 2788 2789 // Hide action boxes on other user pods. 2790 for (var i = 0, p; p = this.pods[i]; ++i) 2791 if (p != pod && !p.isActionBoxMenuActive) 2792 p.isActionBoxMenuHovered = false; 2793 }, 2794 2795 /** 2796 * Handles focus event. 2797 * @param {Event} e Focus Event object. 2798 * @private 2799 */ 2800 handleFocus_: function(e) { 2801 if (this.disabled) 2802 return; 2803 if (e.target.parentNode == this) { 2804 // Focus on a pod 2805 if (e.target.classList.contains('focused')) { 2806 if (!e.target.multiProfilesPolicyApplied) 2807 e.target.focusInput(); 2808 else 2809 e.target.userTypeBubbleElement.classList.add('bubble-shown'); 2810 } else 2811 this.focusPod(e.target); 2812 return; 2813 } 2814 2815 var pod = findAncestorByClass(e.target, 'pod'); 2816 if (pod && pod.parentNode == this) { 2817 // Focus on a control of a pod but not on the action area button. 2818 if (!pod.classList.contains('focused') && 2819 !e.target.classList.contains('action-box-button')) { 2820 this.focusPod(pod); 2821 pod.userTypeBubbleElement.classList.remove('bubble-shown'); 2822 e.target.focus(); 2823 } 2824 return; 2825 } 2826 2827 // Clears pod focus when we reach here. It means new focus is neither 2828 // on a pod nor on a button/input for a pod. 2829 // Do not "defocus" user pod when it is a single pod. 2830 // That means that 'focused' class will not be removed and 2831 // input field/button will always be visible. 2832 if (!this.alwaysFocusSinglePod) 2833 this.focusPod(); 2834 else { 2835 // Hide user-type-bubble in case this is one pod and we lost focus of 2836 // it. 2837 this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown'); 2838 } 2839 }, 2840 2841 /** 2842 * Handler of keydown event. 2843 * @param {Event} e KeyDown Event object. 2844 */ 2845 handleKeyDown: function(e) { 2846 if (this.disabled) 2847 return; 2848 var editing = e.target.tagName == 'INPUT' && e.target.value; 2849 switch (e.keyIdentifier) { 2850 case 'Left': 2851 if (!editing) { 2852 this.keyboardActivated_ = true; 2853 if (this.focusedPod_ && this.focusedPod_.previousElementSibling) 2854 this.focusPod(this.focusedPod_.previousElementSibling); 2855 else 2856 this.focusPod(this.lastElementChild); 2857 2858 e.stopPropagation(); 2859 } 2860 break; 2861 case 'Right': 2862 if (!editing) { 2863 this.keyboardActivated_ = true; 2864 if (this.focusedPod_ && this.focusedPod_.nextElementSibling) 2865 this.focusPod(this.focusedPod_.nextElementSibling); 2866 else 2867 this.focusPod(this.firstElementChild); 2868 2869 e.stopPropagation(); 2870 } 2871 break; 2872 case 'Enter': 2873 if (this.focusedPod_) { 2874 var targetTag = e.target.tagName; 2875 if (e.target == this.focusedPod_.passwordElement || 2876 (targetTag != 'INPUT' && 2877 targetTag != 'BUTTON' && 2878 targetTag != 'A')) { 2879 this.setActivatedPod(this.focusedPod_, e); 2880 e.stopPropagation(); 2881 } 2882 } 2883 break; 2884 case 'U+001B': // Esc 2885 if (!this.alwaysFocusSinglePod) 2886 this.focusPod(); 2887 break; 2888 } 2889 }, 2890 2891 /** 2892 * Called right after the pod row is shown. 2893 */ 2894 handleAfterShow: function() { 2895 // Without timeout changes in pods positions will be animated even though 2896 // it happened when 'flying-pods' class was disabled. 2897 setTimeout(function() { 2898 Oobe.getInstance().toggleClass('flying-pods', true); 2899 }, 0); 2900 // Force input focus for user pod on show and once transition ends. 2901 if (this.focusedPod_) { 2902 var focusedPod = this.focusedPod_; 2903 var screen = this.parentNode; 2904 var self = this; 2905 focusedPod.addEventListener('webkitTransitionEnd', function f(e) { 2906 focusedPod.removeEventListener('webkitTransitionEnd', f); 2907 focusedPod.reset(true); 2908 // Notify screen that it is ready. 2909 screen.onShow(); 2910 }); 2911 // Guard timer for 1 second -- it would conver all possible animations. 2912 ensureTransitionEndEvent(focusedPod, 1000); 2913 } 2914 }, 2915 2916 /** 2917 * Called right before the pod row is shown. 2918 */ 2919 handleBeforeShow: function() { 2920 Oobe.getInstance().toggleClass('flying-pods', false); 2921 for (var event in this.listeners_) { 2922 this.ownerDocument.addEventListener( 2923 event, this.listeners_[event][0], this.listeners_[event][1]); 2924 } 2925 $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR; 2926 2927 if (this.podPlacementPostponed_) { 2928 this.podPlacementPostponed_ = false; 2929 this.placePods_(); 2930 this.maybePreselectPod(); 2931 } 2932 }, 2933 2934 /** 2935 * Called when the element is hidden. 2936 */ 2937 handleHide: function() { 2938 for (var event in this.listeners_) { 2939 this.ownerDocument.removeEventListener( 2940 event, this.listeners_[event][0], this.listeners_[event][1]); 2941 } 2942 $('login-header-bar').buttonsTabIndex = 0; 2943 }, 2944 2945 /** 2946 * Called when a pod's user image finishes loading. 2947 */ 2948 handlePodImageLoad: function(pod) { 2949 var index = this.podsWithPendingImages_.indexOf(pod); 2950 if (index == -1) { 2951 return; 2952 } 2953 2954 this.podsWithPendingImages_.splice(index, 1); 2955 if (this.podsWithPendingImages_.length == 0) { 2956 this.classList.remove('images-loading'); 2957 } 2958 }, 2959 2960 /** 2961 * Preselects pod, if needed. 2962 */ 2963 maybePreselectPod: function() { 2964 var pod = this.preselectedPod; 2965 this.focusPod(pod); 2966 2967 // Hide user-type-bubble in case all user pods are disabled and we focus 2968 // first pod. 2969 if (pod && pod.multiProfilesPolicyApplied) { 2970 pod.userTypeBubbleElement.classList.remove('bubble-shown'); 2971 } 2972 } 2973 }; 2974 2975 return { 2976 PodRow: PodRow 2977 }; 2978}); 2979