Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(56)

Side by Side Diff: chrome/browser/resources/login/user_pod_row.js

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

Powered by Google App Engine
This is Rietveld 408576698