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

Side by Side Diff: ui/login/account_picker/md_user_pod_row.js

Issue 2855883005: cros: Selectively fork login assets. (Closed)
Patch Set: Created 3 years, 7 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
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, 32, 32, 32, 32, 32, 32];
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 CROS_POD_WIDTH = 180;
52 var DESKTOP_POD_WIDTH = 180;
53 var MD_DESKTOP_POD_WIDTH = 160;
54 var PUBLIC_EXPANDED_BASIC_WIDTH = 500;
55 var PUBLIC_EXPANDED_ADVANCED_WIDTH = 610;
56 var CROS_POD_HEIGHT = 213;
57 var DESKTOP_POD_HEIGHT = 226;
58 var MD_DESKTOP_POD_HEIGHT = 200;
59 var POD_ROW_PADDING = 10;
60 var DESKTOP_ROW_PADDING = 32;
61 var CUSTOM_ICON_CONTAINER_SIZE = 40;
62 var CROS_PIN_POD_HEIGHT = 417;
63
64 /**
65 * Minimal padding between user pod and virtual keyboard.
66 * @type {number}
67 * @const
68 */
69 var USER_POD_KEYBOARD_MIN_PADDING = 20;
70
71 /**
72 * Maximum time for which the pod row remains hidden until all user images
73 * have been loaded.
74 * @type {number}
75 * @const
76 */
77 var POD_ROW_IMAGES_LOAD_TIMEOUT_MS = 3000;
78
79 /**
80 * Public session help topic identifier.
81 * @type {number}
82 * @const
83 */
84 var HELP_TOPIC_PUBLIC_SESSION = 3041033;
85
86 /**
87 * Tab order for user pods. Update these when adding new controls.
88 * @enum {number}
89 * @const
90 */
91 var UserPodTabOrder = {
92 POD_INPUT: 1, // Password input field, Action box menu button and
93 // the pod itself.
94 PIN_KEYBOARD: 2, // Pin keyboard below the password input field.
95 POD_CUSTOM_ICON: 3, // Pod custom icon next to password input field.
96 HEADER_BAR: 4, // Buttons on the header bar (Shutdown, Add User).
97 POD_MENU_ITEM: 5 // User pad menu items (User info, Remove 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 FORCE_OFFLINE_PASSWORD: 5
113 };
114
115 /**
116 * Names of authentication types.
117 */
118 var AUTH_TYPE_NAMES = {
119 0: 'offlinePassword',
120 1: 'onlineSignIn',
121 2: 'numericPin',
122 3: 'userClick',
123 4: 'expandThenUserClick',
124 5: 'forceOfflinePassword'
125 };
126
127 /**
128 * Supported fingerprint unlock states.
129 * @enum {number}
130 * @const
131 */
132 var FINGERPRINT_STATES = {
133 HIDDEN: 0,
134 DEFAULT: 1,
135 SIGNIN: 2,
136 FAILED: 3,
137 };
138
139 /**
140 * The fingerprint states to classes mapping.
141 * {@code state} properties indicate current fingerprint unlock state.
142 * {@code class} properties are CSS classes used to set the icons' background
143 * and password placeholder color.
144 * @const {Array<{type: !number, class: !string}>}
145 */
146 var FINGERPRINT_STATES_MAPPING = [
147 {state: FINGERPRINT_STATES.HIDDEN, class: 'hidden'},
148 {state: FINGERPRINT_STATES.DEFAULT, class: 'default'},
149 {state: FINGERPRINT_STATES.SIGNIN, class: 'signin'},
150 {state: FINGERPRINT_STATES.FAILED, class: 'failed'}
151 ];
152
153 // Focus and tab order are organized as follows:
154 //
155 // (1) all user pods have tab index 1 so they are traversed first;
156 // (2) when a user pod is activated, its tab index is set to -1 and its
157 // main input field gets focus and tab index 1;
158 // (3) if user pod custom icon is interactive, it has tab index 2 so it
159 // follows the input.
160 // (4) buttons on the header bar have tab index 3 so they follow the custom
161 // icon, or user pod if custom icon is not interactive;
162 // (5) Action box buttons have tab index 4 and follow header bar buttons;
163 // (6) lastly, focus jumps to the Status Area and back to user pods.
164 //
165 // 'Focus' event is handled by a capture handler for the whole document
166 // and in some cases 'mousedown' event handlers are used instead of 'click'
167 // handlers where it's necessary to prevent 'focus' event from being fired.
168
169 /**
170 * Helper function to remove a class from given element.
171 * @param {!HTMLElement} el Element whose class list to change.
172 * @param {string} cl Class to remove.
173 */
174 function removeClass(el, cl) {
175 el.classList.remove(cl);
176 }
177
178 /**
179 * Creates a user pod.
180 * @constructor
181 * @extends {HTMLDivElement}
182 */
183 var UserPod = cr.ui.define(function() {
184 var node = $('user-pod-template').cloneNode(true);
185 node.removeAttribute('id');
186 return node;
187 });
188
189 /**
190 * Stops event propagation from the any user pod child element.
191 * @param {Event} e Event to handle.
192 */
193 function stopEventPropagation(e) {
194 // Prevent default so that we don't trigger a 'focus' event.
195 e.preventDefault();
196 e.stopPropagation();
197 }
198
199 /**
200 * Creates an element for custom icon shown in a user pod next to the input
201 * field.
202 * @constructor
203 * @extends {HTMLDivElement}
204 */
205 var UserPodCustomIcon = cr.ui.define(function() {
206 var node = document.createElement('div');
207 node.classList.add('custom-icon-container');
208 node.hidden = true;
209
210 // Create the actual icon element and add it as a child to the container.
211 var iconNode = document.createElement('div');
212 iconNode.classList.add('custom-icon');
213 node.appendChild(iconNode);
214 return node;
215 });
216
217 /**
218 * The supported user pod custom icons.
219 * {@code id} properties should be in sync with values set by C++ side.
220 * {@code class} properties are CSS classes used to set the icons' background.
221 * @const {Array<{id: !string, class: !string}>}
222 */
223 UserPodCustomIcon.ICONS = [
224 {id: 'locked', class: 'custom-icon-locked'},
225 {id: 'locked-to-be-activated',
226 class: 'custom-icon-locked-to-be-activated'},
227 {id: 'locked-with-proximity-hint',
228 class: 'custom-icon-locked-with-proximity-hint'},
229 {id: 'unlocked', class: 'custom-icon-unlocked'},
230 {id: 'hardlocked', class: 'custom-icon-hardlocked'},
231 {id: 'spinner', class: 'custom-icon-spinner'}
232 ];
233
234 /**
235 * The hover state for the icon. When user hovers over the icon, a tooltip
236 * should be shown after a short delay. This enum is used to keep track of
237 * the tooltip status related to hover state.
238 * @enum {string}
239 */
240 UserPodCustomIcon.HoverState = {
241 /** The user is not hovering over the icon. */
242 NO_HOVER: 'no_hover',
243
244 /** The user is hovering over the icon but the tooltip is not activated. */
245 HOVER: 'hover',
246
247 /**
248 * User is hovering over the icon and the tooltip is activated due to the
249 * hover state (which happens with delay after user starts hovering).
250 */
251 HOVER_TOOLTIP: 'hover_tooltip'
252 };
253
254 /**
255 * If the icon has a tooltip that should be automatically shown, the tooltip
256 * is shown even when there is no user action (i.e. user is not hovering over
257 * the icon), after a short delay. The tooltip should be hidden after some
258 * time. Note that the icon will not be considered autoshown if it was
259 * previously shown as a result of the user action.
260 * This enum is used to keep track of this state.
261 * @enum {string}
262 */
263 UserPodCustomIcon.TooltipAutoshowState = {
264 /** The tooltip should not be or was not automatically shown. */
265 DISABLED: 'disabled',
266
267 /**
268 * The tooltip should be automatically shown, but the timeout for showing
269 * the tooltip has not yet passed.
270 */
271 ENABLED: 'enabled',
272
273 /** The tooltip was automatically shown. */
274 ACTIVE : 'active'
275 };
276
277 UserPodCustomIcon.prototype = {
278 __proto__: HTMLDivElement.prototype,
279
280 /**
281 * The id of the icon being shown.
282 * @type {string}
283 * @private
284 */
285 iconId_: '',
286
287 /**
288 * A reference to the timeout for updating icon hover state. Non-null
289 * only if there is an active timeout.
290 * @type {?number}
291 * @private
292 */
293 updateHoverStateTimeout_: null,
294
295 /**
296 * A reference to the timeout for updating icon tooltip autoshow state.
297 * Non-null only if there is an active timeout.
298 * @type {?number}
299 * @private
300 */
301 updateTooltipAutoshowStateTimeout_: null,
302
303 /**
304 * Callback for click and 'Enter' key events that gets set if the icon is
305 * interactive.
306 * @type {?function()}
307 * @private
308 */
309 actionHandler_: null,
310
311 /**
312 * The current tooltip state.
313 * @type {{active: function(): boolean,
314 * autoshow: !UserPodCustomIcon.TooltipAutoshowState,
315 * hover: !UserPodCustomIcon.HoverState,
316 * text: string}}
317 * @private
318 */
319 tooltipState_: {
320 /**
321 * Utility method for determining whether the tooltip is active, either as
322 * a result of hover state or being autoshown.
323 * @return {boolean}
324 */
325 active: function() {
326 return this.autoshow == UserPodCustomIcon.TooltipAutoshowState.ACTIVE ||
327 this.hover == UserPodCustomIcon.HoverState.HOVER_TOOLTIP;
328 },
329
330 /**
331 * @type {!UserPodCustomIcon.TooltipAutoshowState}
332 */
333 autoshow: UserPodCustomIcon.TooltipAutoshowState.DISABLED,
334
335 /**
336 * @type {!UserPodCustomIcon.HoverState}
337 */
338 hover: UserPodCustomIcon.HoverState.NO_HOVER,
339
340 /**
341 * The tooltip text.
342 * @type {string}
343 */
344 text: ''
345 },
346
347 /** @override */
348 decorate: function() {
349 this.iconElement.addEventListener(
350 'mouseover',
351 this.updateHoverState_.bind(this,
352 UserPodCustomIcon.HoverState.HOVER));
353 this.iconElement.addEventListener(
354 'mouseout',
355 this.updateHoverState_.bind(this,
356 UserPodCustomIcon.HoverState.NO_HOVER));
357 this.iconElement.addEventListener('mousedown',
358 this.handleMouseDown_.bind(this));
359 this.iconElement.addEventListener('click',
360 this.handleClick_.bind(this));
361 this.iconElement.addEventListener('keydown',
362 this.handleKeyDown_.bind(this));
363
364 // When the icon is focused using mouse, there should be no outline shown.
365 // Preventing default mousedown event accomplishes this.
366 this.iconElement.addEventListener('mousedown', function(e) {
367 e.preventDefault();
368 });
369 },
370
371 /**
372 * Getter for the icon element's div.
373 * @return {HTMLDivElement}
374 */
375 get iconElement() {
376 return this.querySelector('.custom-icon');
377 },
378
379 /**
380 * Updates the icon element class list to properly represent the provided
381 * icon.
382 * @param {!string} id The id of the icon that should be shown. Should be
383 * one of the ids listed in {@code UserPodCustomIcon.ICONS}.
384 */
385 setIcon: function(id) {
386 this.iconId_ = id;
387 UserPodCustomIcon.ICONS.forEach(function(icon) {
388 this.iconElement.classList.toggle(icon.class, id == icon.id);
389 }, this);
390 },
391
392 /**
393 * Sets the ARIA label for the icon.
394 * @param {!string} ariaLabel
395 */
396 setAriaLabel: function(ariaLabel) {
397 this.iconElement.setAttribute('aria-label', ariaLabel);
398 },
399
400 /**
401 * Shows the icon.
402 */
403 show: function() {
404 // Show the icon if the current iconId is valid.
405 var validIcon = false;
406 UserPodCustomIcon.ICONS.forEach(function(icon) {
407 validIcon = validIcon || this.iconId_ == icon.id;
408 }, this);
409 this.hidden = validIcon ? false : true;
410 },
411
412 /**
413 * Updates the icon tooltip. If {@code autoshow} parameter is set the
414 * tooltip is immediatelly shown. If tooltip text is not set, the method
415 * ensures the tooltip gets hidden. If tooltip is shown prior to this call,
416 * it remains shown, but the tooltip text is updated.
417 * @param {!{text: string, autoshow: boolean}} tooltip The tooltip
418 * parameters.
419 */
420 setTooltip: function(tooltip) {
421 this.iconElement.classList.toggle('icon-with-tooltip', !!tooltip.text);
422
423 this.updateTooltipAutoshowState_(
424 tooltip.autoshow ?
425 UserPodCustomIcon.TooltipAutoshowState.ENABLED :
426 UserPodCustomIcon.TooltipAutoshowState.DISABLED);
427 this.tooltipState_.text = tooltip.text;
428 this.updateTooltip_();
429 },
430
431 /**
432 * Sets up icon tabIndex attribute and handler for click and 'Enter' key
433 * down events.
434 * @param {?function()} callback If icon should be interactive, the
435 * function to get called on click and 'Enter' key down events. Should
436 * be null to make the icon non interactive.
437 */
438 setInteractive: function(callback) {
439 this.iconElement.classList.toggle('interactive-custom-icon', !!callback);
440
441 // Update tabIndex property if needed.
442 if (!!this.actionHandler_ != !!callback) {
443 if (callback) {
444 this.iconElement.setAttribute('tabIndex',
445 UserPodTabOrder.POD_CUSTOM_ICON);
446 } else {
447 this.iconElement.removeAttribute('tabIndex');
448 }
449 }
450
451 // Set the new action handler.
452 this.actionHandler_ = callback;
453 },
454
455 /**
456 * Hides the icon and cleans its state.
457 */
458 hide: function() {
459 this.hideTooltip_();
460 this.clearUpdateHoverStateTimeout_();
461 this.clearUpdateTooltipAutoshowStateTimeout_();
462 this.setInteractive(null);
463 this.hidden = true;
464 },
465
466 /**
467 * Clears timeout for showing a tooltip if one is set. Used to cancel
468 * showing the tooltip when the user starts typing the password.
469 */
470 cancelDelayedTooltipShow: function() {
471 this.updateTooltipAutoshowState_(
472 UserPodCustomIcon.TooltipAutoshowState.DISABLED);
473 this.clearUpdateHoverStateTimeout_();
474 },
475
476 /**
477 * Handles mouse down event in the icon element.
478 * @param {Event} e The mouse down event.
479 * @private
480 */
481 handleMouseDown_: function(e) {
482 this.updateHoverState_(UserPodCustomIcon.HoverState.NO_HOVER);
483 this.updateTooltipAutoshowState_(
484 UserPodCustomIcon.TooltipAutoshowState.DISABLED);
485
486 // Stop the event propagation so in the case the click ends up on the
487 // user pod (outside the custom icon) auth is not attempted.
488 stopEventPropagation(e);
489 },
490
491 /**
492 * Handles click event on the icon element. No-op if
493 * {@code this.actionHandler_} is not set.
494 * @param {Event} e The click event.
495 * @private
496 */
497 handleClick_: function(e) {
498 if (!this.actionHandler_)
499 return;
500 this.actionHandler_();
501 stopEventPropagation(e);
502 },
503
504 /**
505 * Handles key down event on the icon element. Only 'Enter' key is handled.
506 * No-op if {@code this.actionHandler_} is not set.
507 * @param {Event} e The key down event.
508 * @private
509 */
510 handleKeyDown_: function(e) {
511 if (!this.actionHandler_ || e.key != 'Enter')
512 return;
513 this.actionHandler_(e);
514 stopEventPropagation(e);
515 },
516
517 /**
518 * Changes the tooltip hover state and updates tooltip visibility if needed.
519 * @param {!UserPodCustomIcon.HoverState} state
520 * @private
521 */
522 updateHoverState_: function(state) {
523 this.clearUpdateHoverStateTimeout_();
524 this.sanitizeTooltipStateIfBubbleHidden_();
525
526 if (state == UserPodCustomIcon.HoverState.HOVER) {
527 if (this.tooltipState_.active()) {
528 this.tooltipState_.hover = UserPodCustomIcon.HoverState.HOVER_TOOLTIP;
529 } else {
530 this.updateHoverStateSoon_(
531 UserPodCustomIcon.HoverState.HOVER_TOOLTIP);
532 }
533 return;
534 }
535
536 if (state != UserPodCustomIcon.HoverState.NO_HOVER &&
537 state != UserPodCustomIcon.HoverState.HOVER_TOOLTIP) {
538 console.error('Invalid hover state ' + state);
539 return;
540 }
541
542 this.tooltipState_.hover = state;
543 this.updateTooltip_();
544 },
545
546 /**
547 * Sets up a timeout for updating icon hover state.
548 * @param {!UserPodCustomIcon.HoverState} state
549 * @private
550 */
551 updateHoverStateSoon_: function(state) {
552 if (this.updateHoverStateTimeout_)
553 clearTimeout(this.updateHoverStateTimeout_);
554 this.updateHoverStateTimeout_ =
555 setTimeout(this.updateHoverState_.bind(this, state), 1000);
556 },
557
558 /**
559 * Clears a timeout for updating icon hover state if there is one set.
560 * @private
561 */
562 clearUpdateHoverStateTimeout_: function() {
563 if (this.updateHoverStateTimeout_) {
564 clearTimeout(this.updateHoverStateTimeout_);
565 this.updateHoverStateTimeout_ = null;
566 }
567 },
568
569 /**
570 * Changes the tooltip autoshow state and changes tooltip visibility if
571 * needed.
572 * @param {!UserPodCustomIcon.TooltipAutoshowState} state
573 * @private
574 */
575 updateTooltipAutoshowState_: function(state) {
576 this.clearUpdateTooltipAutoshowStateTimeout_();
577 this.sanitizeTooltipStateIfBubbleHidden_();
578
579 if (state == UserPodCustomIcon.TooltipAutoshowState.DISABLED) {
580 if (this.tooltipState_.autoshow != state) {
581 this.tooltipState_.autoshow = state;
582 this.updateTooltip_();
583 }
584 return;
585 }
586
587 if (this.tooltipState_.active()) {
588 if (this.tooltipState_.autoshow !=
589 UserPodCustomIcon.TooltipAutoshowState.ACTIVE) {
590 this.tooltipState_.autoshow =
591 UserPodCustomIcon.TooltipAutoshowState.DISABLED;
592 } else {
593 // If the tooltip is already automatically shown, the timeout for
594 // removing it should be reset.
595 this.updateTooltipAutoshowStateSoon_(
596 UserPodCustomIcon.TooltipAutoshowState.DISABLED);
597 }
598 return;
599 }
600
601 if (state == UserPodCustomIcon.TooltipAutoshowState.ENABLED) {
602 this.updateTooltipAutoshowStateSoon_(
603 UserPodCustomIcon.TooltipAutoshowState.ACTIVE);
604 } else if (state == UserPodCustomIcon.TooltipAutoshowState.ACTIVE) {
605 this.updateTooltipAutoshowStateSoon_(
606 UserPodCustomIcon.TooltipAutoshowState.DISABLED);
607 }
608
609 this.tooltipState_.autoshow = state;
610 this.updateTooltip_();
611 },
612
613 /**
614 * Sets up a timeout for updating tooltip autoshow state.
615 * @param {!UserPodCustomIcon.TooltipAutoshowState} state
616 * @private
617 */
618 updateTooltipAutoshowStateSoon_: function(state) {
619 if (this.updateTooltipAutoshowStateTimeout_)
620 clearTimeout(this.updateTooltupAutoshowStateTimeout_);
621 var timeout =
622 state == UserPodCustomIcon.TooltipAutoshowState.DISABLED ?
623 5000 : 1000;
624 this.updateTooltipAutoshowStateTimeout_ =
625 setTimeout(this.updateTooltipAutoshowState_.bind(this, state),
626 timeout);
627 },
628
629 /**
630 * Clears the timeout for updating tooltip autoshow state if one is set.
631 * @private
632 */
633 clearUpdateTooltipAutoshowStateTimeout_: function() {
634 if (this.updateTooltipAutoshowStateTimeout_) {
635 clearTimeout(this.updateTooltipAutoshowStateTimeout_);
636 this.updateTooltipAutoshowStateTimeout_ = null;
637 }
638 },
639
640 /**
641 * If tooltip bubble is hidden, this makes sure that hover and tooltip
642 * autoshow states are not the ones that imply an active tooltip.
643 * Used to handle a case where the tooltip bubble is hidden by an event that
644 * does not update one of the states (e.g. click outside the pod will not
645 * update tooltip autoshow state). Should be called before making
646 * tooltip state updates.
647 * @private
648 */
649 sanitizeTooltipStateIfBubbleHidden_: function() {
650 if (!$('bubble').hidden)
651 return;
652
653 if (this.tooltipState_.hover ==
654 UserPodCustomIcon.HoverState.HOVER_TOOLTIP &&
655 this.tooltipState_.text) {
656 this.tooltipState_.hover = UserPodCustomIcon.HoverState.NO_HOVER;
657 this.clearUpdateHoverStateTimeout_();
658 }
659
660 if (this.tooltipState_.autoshow ==
661 UserPodCustomIcon.TooltipAutoshowState.ACTIVE) {
662 this.tooltipState_.autoshow =
663 UserPodCustomIcon.TooltipAutoshowState.DISABLED;
664 this.clearUpdateTooltipAutoshowStateTimeout_();
665 }
666 },
667
668 /**
669 * Returns whether the user pod to which the custom icon belongs is focused.
670 * @return {boolean}
671 * @private
672 */
673 isParentPodFocused_: function() {
674 if ($('account-picker').hidden)
675 return false;
676 var parentPod = this.parentNode;
677 while (parentPod && !parentPod.classList.contains('pod'))
678 parentPod = parentPod.parentNode;
679 return parentPod && parentPod.parentNode.isFocused(parentPod);
680 },
681
682 /**
683 * Depending on {@code this.tooltipState_}, it updates tooltip visibility
684 * and text.
685 * @private
686 */
687 updateTooltip_: function() {
688 if (this.hidden || !this.isParentPodFocused_())
689 return;
690
691 if (!this.tooltipState_.active() || !this.tooltipState_.text) {
692 this.hideTooltip_();
693 return;
694 }
695
696 // Show the tooltip bubble.
697 var bubbleContent = document.createElement('div');
698 bubbleContent.textContent = this.tooltipState_.text;
699
700 /** @const */ var BUBBLE_OFFSET = CUSTOM_ICON_CONTAINER_SIZE / 2;
701 // TODO(tengs): Introduce a special reauth state for the account picker,
702 // instead of showing the tooltip bubble here (crbug.com/409427).
703 /** @const */ var BUBBLE_PADDING = 8 + (this.iconId_ ? 0 : 23);
704 $('bubble').showContentForElement(this,
705 cr.ui.Bubble.Attachment.LEFT,
706 bubbleContent,
707 BUBBLE_OFFSET,
708 BUBBLE_PADDING);
709 },
710
711 /**
712 * Hides the tooltip.
713 * @private
714 */
715 hideTooltip_: function() {
716 $('bubble').hideForElement(this);
717 }
718 };
719
720 /**
721 * Unique salt added to user image URLs to prevent caching. Dictionary with
722 * user names as keys.
723 * @type {Object}
724 */
725 UserPod.userImageSalt_ = {};
726
727 UserPod.prototype = {
728 __proto__: HTMLDivElement.prototype,
729
730 /**
731 * Whether click on the pod can issue a user click auth attempt. The
732 * attempt can be issued iff the pod was focused when the click
733 * started (i.e. on mouse down event).
734 * @type {boolean}
735 * @private
736 */
737 userClickAuthAllowed_: false,
738
739 /**
740 * Whether the user has recently authenticated with fingerprint.
741 * @type {boolean}
742 * @private
743 */
744 fingerprintAuthenticated_: false,
745
746 /**
747 * True iff the pod can display the pin keyboard. The pin keyboard may not
748 * always be displayed even if this is true, ie, if the virtual keyboard is
749 * also being displayed.
750 */
751 pinEnabled: false,
752
753 /** @override */
754 decorate: function() {
755 this.tabIndex = UserPodTabOrder.POD_INPUT;
756 this.actionBoxAreaElement.tabIndex = UserPodTabOrder.POD_INPUT;
757
758 this.addEventListener('keydown', this.handlePodKeyDown_.bind(this));
759 this.addEventListener('click', this.handleClickOnPod_.bind(this));
760 this.addEventListener('mousedown', this.handlePodMouseDown_.bind(this));
761
762 if (this.pinKeyboard) {
763 this.pinKeyboard.passwordElement = this.passwordElement;
764 this.pinKeyboard.addEventListener('pin-change',
765 this.handleInputChanged_.bind(this));
766 this.pinKeyboard.tabIndex = UserPodTabOrder.PIN_KEYBOARD;
767 }
768
769 this.actionBoxAreaElement.addEventListener('mousedown',
770 stopEventPropagation);
771 this.actionBoxAreaElement.addEventListener('click',
772 this.handleActionAreaButtonClick_.bind(this));
773 this.actionBoxAreaElement.addEventListener('keydown',
774 this.handleActionAreaButtonKeyDown_.bind(this));
775
776 this.actionBoxMenuTitleElement.addEventListener('keydown',
777 this.handleMenuTitleElementKeyDown_.bind(this));
778 this.actionBoxMenuTitleElement.addEventListener('blur',
779 this.handleMenuTitleElementBlur_.bind(this));
780
781 this.actionBoxMenuRemoveElement.addEventListener('click',
782 this.handleRemoveCommandClick_.bind(this));
783 this.actionBoxMenuRemoveElement.addEventListener('keydown',
784 this.handleRemoveCommandKeyDown_.bind(this));
785 this.actionBoxMenuRemoveElement.addEventListener('blur',
786 this.handleRemoveCommandBlur_.bind(this));
787 this.actionBoxRemoveUserWarningButtonElement.addEventListener('click',
788 this.handleRemoveUserConfirmationClick_.bind(this));
789 this.actionBoxRemoveUserWarningButtonElement.addEventListener('keydown',
790 this.handleRemoveUserConfirmationKeyDown_.bind(this));
791
792 if (this.fingerprintIconElement) {
793 this.fingerprintIconElement.addEventListener(
794 'mouseover', this.handleFingerprintIconMouseOver_.bind(this));
795 this.fingerprintIconElement.addEventListener(
796 'mouseout', this.handleFingerprintIconMouseOut_.bind(this));
797 this.fingerprintIconElement.addEventListener(
798 'mousedown', stopEventPropagation);
799 }
800
801 var customIcon = this.customIconElement;
802 customIcon.parentNode.replaceChild(new UserPodCustomIcon(), customIcon);
803 },
804
805 /**
806 * Initializes the pod after its properties set and added to a pod row.
807 */
808 initialize: function() {
809 this.passwordElement.addEventListener('keydown',
810 this.parentNode.handleKeyDown.bind(this.parentNode));
811 this.passwordElement.addEventListener('keypress',
812 this.handlePasswordKeyPress_.bind(this));
813 this.passwordElement.addEventListener('input',
814 this.handleInputChanged_.bind(this));
815 this.passwordElement.addEventListener('mouseup',
816 this.handleInputMouseUp_.bind(this));
817
818 if (this.submitButton) {
819 this.submitButton.addEventListener('click',
820 this.handleSubmitButtonClick_.bind(this));
821 }
822
823 this.imageElement.addEventListener('load',
824 this.parentNode.handlePodImageLoad.bind(this.parentNode, this));
825
826 var initialAuthType = this.user.initialAuthType ||
827 AUTH_TYPE.OFFLINE_PASSWORD;
828 this.setAuthType(initialAuthType, null);
829
830 if (this.user.isActiveDirectory)
831 this.setAttribute('is-active-directory', '');
832
833 this.userClickAuthAllowed_ = false;
834
835 // Lazy load the assets needed for the polymer submit button.
836 var isLockScreen = (Oobe.getInstance().displayType == DISPLAY_TYPE.LOCK);
837 if (cr.isChromeOS && isLockScreen &&
838 !cr.ui.login.ResourceLoader.alreadyLoadedAssets(
839 'custom-elements-user-pod')) {
840 cr.ui.login.ResourceLoader.registerAssets({
841 id: 'custom-elements-user-pod',
842 html: [{ url: 'custom_elements_user_pod.html' }]
843 });
844 cr.ui.login.ResourceLoader.loadAssetsOnIdle('custom-elements-user-pod');
845 }
846 },
847
848 /**
849 * Whether the user pod is disabled.
850 * @type {boolean}
851 */
852 disabled_: false,
853 get disabled() {
854 return this.disabled_;
855 },
856 set disabled(value) {
857 this.disabled_ = value;
858 this.querySelectorAll('button,input').forEach(function(element) {
859 element.disabled = value
860 });
861
862 // Special handling for submit button - the submit button should be
863 // enabled only if there is the password value set.
864 var submitButton = this.submitButton;
865 if (submitButton)
866 submitButton.disabled = value || !this.passwordElement.value;
867 },
868
869 /**
870 * Resets tab order for pod elements to its initial state.
871 */
872 resetTabOrder: function() {
873 // Note: the |mainInput| can be the pod itself.
874 this.mainInput.tabIndex = -1;
875 this.tabIndex = UserPodTabOrder.POD_INPUT;
876 },
877
878 /**
879 * Handles keypress event (i.e. any textual input) on password input.
880 * @param {Event} e Keypress Event object.
881 * @private
882 */
883 handlePasswordKeyPress_: function(e) {
884 // When tabbing from the system tray a tab key press is received. Suppress
885 // this so as not to type a tab character into the password field.
886 if (e.keyCode == 9) {
887 e.preventDefault();
888 return;
889 }
890 this.customIconElement.cancelDelayedTooltipShow();
891 },
892
893 /**
894 * Handles a click event on submit button.
895 * @param {Event} e Click event.
896 */
897 handleSubmitButtonClick_: function(e) {
898 this.parentNode.setActivatedPod(this, e);
899 },
900
901 /**
902 * Top edge margin number of pixels.
903 * @type {?number}
904 */
905 set top(top) {
906 this.style.top = cr.ui.toCssPx(top);
907 },
908
909 /**
910 * Top edge margin number of pixels.
911 */
912 get top() {
913 return parseInt(this.style.top);
914 },
915
916 /**
917 * Left edge margin number of pixels.
918 * @type {?number}
919 */
920 set left(left) {
921 this.style.left = cr.ui.toCssPx(left);
922 },
923
924 /**
925 * Left edge margin number of pixels.
926 */
927 get left() {
928 return parseInt(this.style.left);
929 },
930
931 /**
932 * Height number of pixels.
933 */
934 get height() {
935 return this.offsetHeight;
936 },
937
938 /**
939 * Gets image element.
940 * @type {!HTMLImageElement}
941 */
942 get imageElement() {
943 return this.querySelector('.user-image');
944 },
945
946 /**
947 * Gets name element.
948 * @type {!HTMLDivElement}
949 */
950 get nameElement() {
951 return this.querySelector('.name');
952 },
953
954 /**
955 * Gets reauth name hint element.
956 * @type {!HTMLDivElement}
957 */
958 get reauthNameHintElement() {
959 return this.querySelector('.reauth-name-hint');
960 },
961
962 /**
963 * Gets the container holding the password field.
964 * @type {!HTMLInputElement}
965 */
966 get passwordEntryContainerElement() {
967 return this.querySelector('.password-entry-container');
968 },
969
970 /**
971 * Gets password field.
972 * @type {!HTMLInputElement}
973 */
974 get passwordElement() {
975 return this.querySelector('.password');
976 },
977
978 /**
979 * Gets submit button.
980 * @type {!HTMLInputElement}
981 */
982 get submitButton() {
983 return this.querySelector('.submit-button');
984 },
985
986 /**
987 * Gets the password label, which is used to show a message where the
988 * password field is normally.
989 * @type {!HTMLInputElement}
990 */
991 get passwordLabelElement() {
992 return this.querySelector('.password-label');
993 },
994
995 get pinContainer() {
996 return this.querySelector('.pin-container');
997 },
998
999 /**
1000 * Gets the pin-keyboard of the pod.
1001 * @type {!HTMLElement}
1002 */
1003 get pinKeyboard() {
1004 return this.querySelector('pin-keyboard');
1005 },
1006
1007 /**
1008 * Gets user online sign in hint element.
1009 * @type {!HTMLDivElement}
1010 */
1011 get reauthWarningElement() {
1012 return this.querySelector('.reauth-hint-container');
1013 },
1014
1015 /**
1016 * Gets the container holding the launch app button.
1017 * @type {!HTMLButtonElement}
1018 */
1019 get launchAppButtonContainerElement() {
1020 return this.querySelector('.launch-app-button-container');
1021 },
1022
1023 /**
1024 * Gets launch app button.
1025 * @type {!HTMLButtonElement}
1026 */
1027 get launchAppButtonElement() {
1028 return this.querySelector('.launch-app-button');
1029 },
1030
1031 /**
1032 * Gets action box area.
1033 * @type {!HTMLInputElement}
1034 */
1035 get actionBoxAreaElement() {
1036 return this.querySelector('.action-box-area');
1037 },
1038
1039 /**
1040 * Gets user type icon area.
1041 * @type {!HTMLDivElement}
1042 */
1043 get userTypeIconAreaElement() {
1044 return this.querySelector('.user-type-icon-area');
1045 },
1046
1047 /**
1048 * Gets user type bubble like multi-profiles policy restriction message.
1049 * @type {!HTMLDivElement}
1050 */
1051 get userTypeBubbleElement() {
1052 return this.querySelector('.user-type-bubble');
1053 },
1054
1055 /**
1056 * Gets action box menu.
1057 * @type {!HTMLDivElement}
1058 */
1059 get actionBoxMenu() {
1060 return this.querySelector('.action-box-menu');
1061 },
1062
1063 /**
1064 * Gets action box menu title (user name and email).
1065 * @type {!HTMLDivElement}
1066 */
1067 get actionBoxMenuTitleElement() {
1068 return this.querySelector('.action-box-menu-title');
1069 },
1070
1071 /**
1072 * Gets action box menu title, user name item.
1073 * @type {!HTMLSpanElement}
1074 */
1075 get actionBoxMenuTitleNameElement() {
1076 return this.querySelector('.action-box-menu-title-name');
1077 },
1078
1079 /**
1080 * Gets action box menu title, user email item.
1081 * @type {!HTMLSpanElement}
1082 */
1083 get actionBoxMenuTitleEmailElement() {
1084 return this.querySelector('.action-box-menu-title-email');
1085 },
1086
1087 /**
1088 * Gets action box menu, remove user command item.
1089 * @type {!HTMLInputElement}
1090 */
1091 get actionBoxMenuCommandElement() {
1092 return this.querySelector('.action-box-menu-remove-command');
1093 },
1094
1095 /**
1096 * Gets action box menu, remove user command item div.
1097 * @type {!HTMLInputElement}
1098 */
1099 get actionBoxMenuRemoveElement() {
1100 return this.querySelector('.action-box-menu-remove');
1101 },
1102
1103 /**
1104 * Gets action box menu, remove user command item div.
1105 * @type {!HTMLInputElement}
1106 */
1107 get actionBoxRemoveUserWarningElement() {
1108 return this.querySelector('.action-box-remove-user-warning');
1109 },
1110
1111 /**
1112 * Gets action box menu, remove user command item div.
1113 * @type {!HTMLInputElement}
1114 */
1115 get actionBoxRemoveUserWarningButtonElement() {
1116 return this.querySelector('.remove-warning-button');
1117 },
1118
1119 /**
1120 * Gets the custom icon. This icon is normally hidden, but can be shown
1121 * using the chrome.screenlockPrivate API.
1122 * @type {!HTMLDivElement}
1123 */
1124 get customIconElement() {
1125 return this.querySelector('.custom-icon-container');
1126 },
1127
1128 /**
1129 * Gets the elements used for statistics display.
1130 * @type {Object.<string, !HTMLDivElement>}
1131 */
1132 get statsMapElements() {
1133 return {
1134 'BrowsingHistory':
1135 this.querySelector('.action-box-remove-user-warning-history'),
1136 'Passwords':
1137 this.querySelector('.action-box-remove-user-warning-passwords'),
1138 'Bookmarks':
1139 this.querySelector('.action-box-remove-user-warning-bookmarks'),
1140 'Settings':
1141 this.querySelector('.action-box-remove-user-warning-settings')
1142 }
1143 },
1144
1145 /**
1146 * Gets the fingerprint icon area.
1147 * @type {!HTMLDivElement}
1148 */
1149 get fingerprintIconElement() {
1150 return this.querySelector('.fingerprint-icon-container');
1151 },
1152
1153 /**
1154 * Updates the user pod element.
1155 */
1156 update: function() {
1157 this.imageElement.src = 'chrome://userimage/' + this.user.username +
1158 '?id=' + UserPod.userImageSalt_[this.user.username];
1159
1160 this.nameElement.textContent = this.user_.displayName;
1161 this.reauthNameHintElement.textContent = this.user_.displayName;
1162 this.classList.toggle('signed-in', this.user_.signedIn);
1163
1164 if (this.isAuthTypeUserClick)
1165 this.passwordLabelElement.textContent = this.authValue;
1166
1167 this.updateActionBoxArea();
1168
1169 this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
1170 'passwordFieldAccessibleName', this.user_.emailAddress));
1171
1172 this.customizeUserPodPerUserType();
1173 },
1174
1175 updateActionBoxArea: function() {
1176 if (this.user_.publicAccount || this.user_.isApp) {
1177 this.actionBoxAreaElement.hidden = true;
1178 return;
1179 }
1180
1181 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
1182
1183 this.actionBoxAreaElement.setAttribute(
1184 'aria-label', loadTimeData.getStringF(
1185 'podMenuButtonAccessibleName', this.user_.emailAddress));
1186 this.actionBoxMenuRemoveElement.setAttribute(
1187 'aria-label', loadTimeData.getString(
1188 'podMenuRemoveItemAccessibleName'));
1189 this.actionBoxMenuTitleNameElement.textContent = this.user_.isOwner ?
1190 loadTimeData.getStringF('ownerUserPattern', this.user_.displayName) :
1191 this.user_.displayName;
1192 this.actionBoxMenuTitleEmailElement.textContent = this.user_.emailAddress;
1193
1194 this.actionBoxMenuTitleEmailElement.hidden =
1195 this.user_.legacySupervisedUser;
1196
1197 this.actionBoxMenuCommandElement.textContent =
1198 loadTimeData.getString('removeUser');
1199 },
1200
1201 customizeUserPodPerUserType: function() {
1202 if (this.user_.childUser && !this.user_.isDesktopUser) {
1203 this.setUserPodIconType('child');
1204 } else if (this.user_.legacySupervisedUser && !this.user_.isDesktopUser) {
1205 this.setUserPodIconType('legacySupervised');
1206 this.classList.add('legacy-supervised');
1207 } else if (this.multiProfilesPolicyApplied) {
1208 // Mark user pod as not focusable which in addition to the grayed out
1209 // filter makes it look in disabled state.
1210 this.classList.add('multiprofiles-policy-applied');
1211 this.setUserPodIconType('policy');
1212
1213 if (this.user.multiProfilesPolicy == 'primary-only')
1214 this.querySelector('.mp-policy-primary-only-msg').hidden = false;
1215 else if (this.user.multiProfilesPolicy == 'owner-primary-only')
1216 this.querySelector('.mp-owner-primary-only-msg').hidden = false;
1217 else
1218 this.querySelector('.mp-policy-not-allowed-msg').hidden = false;
1219 } else if (this.user_.isApp) {
1220 this.setUserPodIconType('app');
1221 }
1222 },
1223
1224 isPinReady: function() {
1225 return this.pinKeyboard && this.pinKeyboard.offsetHeight > 0;
1226 },
1227
1228 set showError(visible) {
1229 if (this.submitButton)
1230 this.submitButton.classList.toggle('error-shown', visible);
1231 },
1232
1233 updatePinClass_: function(element, enable) {
1234 element.classList.toggle('pin-enabled', enable);
1235 element.classList.toggle('pin-disabled', !enable);
1236 },
1237
1238 setPinVisibility: function(visible) {
1239 if (this.isPinShown() == visible)
1240 return;
1241
1242 // Do not show pin if virtual keyboard is there.
1243 if (visible && Oobe.getInstance().virtualKeyboardShown)
1244 return;
1245
1246 // Do not show pin keyboard if the pod does not have pin enabled.
1247 if (visible && !this.pinEnabled)
1248 return;
1249
1250 var elements = this.getElementsByClassName('pin-tag');
1251 for (var i = 0; i < elements.length; ++i)
1252 this.updatePinClass_(elements[i], visible);
1253 this.updatePinClass_(this, visible);
1254
1255 // Set the focus to the input element after showing/hiding pin keyboard.
1256 this.mainInput.focus();
1257
1258 // Change the password placeholder based on pin keyboard visibility.
1259 this.passwordElement.placeholder = loadTimeData.getString(visible ?
1260 'pinKeyboardPlaceholderPinPassword' : 'passwordHint');
1261
1262 chrome.send('setForceDisableVirtualKeyboard', [visible]);
1263 },
1264
1265 isPinShown: function() {
1266 return this.classList.contains('pin-enabled');
1267 },
1268
1269 setUserPodIconType: function(userTypeClass) {
1270 this.userTypeIconAreaElement.classList.add(userTypeClass);
1271 this.userTypeIconAreaElement.hidden = false;
1272 },
1273
1274 isFingerprintIconShown: function() {
1275 return this.fingerprintIconElement && !this.fingerprintIconElement.hidden;
1276 },
1277
1278 /**
1279 * The user that this pod represents.
1280 * @type {!Object}
1281 */
1282 user_: undefined,
1283 get user() {
1284 return this.user_;
1285 },
1286 set user(userDict) {
1287 this.user_ = userDict;
1288 this.update();
1289 },
1290
1291 /**
1292 * Returns true if multi-profiles sign in is currently active and this
1293 * user pod is restricted per policy.
1294 * @type {boolean}
1295 */
1296 get multiProfilesPolicyApplied() {
1297 var isMultiProfilesUI =
1298 (Oobe.getInstance().displayType == DISPLAY_TYPE.USER_ADDING);
1299 return isMultiProfilesUI && !this.user_.isMultiProfilesAllowed;
1300 },
1301
1302 /**
1303 * Gets main input element.
1304 * @type {(HTMLButtonElement|HTMLInputElement)}
1305 */
1306 get mainInput() {
1307 if (this.isAuthTypePassword) {
1308 return this.passwordElement;
1309 } else if (this.isAuthTypeOnlineSignIn) {
1310 return this;
1311 } else if (this.isAuthTypeUserClick) {
1312 return this.passwordLabelElement;
1313 }
1314 },
1315
1316 /**
1317 * Whether action box button is in active state.
1318 * @type {boolean}
1319 */
1320 get isActionBoxMenuActive() {
1321 return this.actionBoxAreaElement.classList.contains('active');
1322 },
1323 set isActionBoxMenuActive(active) {
1324 if (active == this.isActionBoxMenuActive)
1325 return;
1326
1327 if (active) {
1328 this.actionBoxMenuRemoveElement.hidden = !this.user_.canRemove;
1329 this.actionBoxRemoveUserWarningElement.hidden = true;
1330
1331 // Clear focus first if another pod is focused.
1332 if (!this.parentNode.isFocused(this)) {
1333 this.parentNode.focusPod(undefined, true);
1334 this.actionBoxAreaElement.focus();
1335 }
1336
1337 // Hide user-type-bubble.
1338 this.userTypeBubbleElement.classList.remove('bubble-shown');
1339
1340 this.actionBoxAreaElement.classList.add('active');
1341
1342 // Invisible focus causes ChromeVox to read user name and email.
1343 this.actionBoxMenuTitleElement.tabIndex = UserPodTabOrder.POD_MENU_ITEM;
1344 this.actionBoxMenuTitleElement.focus();
1345
1346 // If the user pod is on either edge of the screen, then the menu
1347 // could be displayed partially ofscreen.
1348 this.actionBoxMenu.classList.remove('left-edge-offset');
1349 this.actionBoxMenu.classList.remove('right-edge-offset');
1350
1351 var offsetLeft =
1352 cr.ui.login.DisplayManager.getOffset(this.actionBoxMenu).left;
1353 var menuWidth = this.actionBoxMenu.offsetWidth;
1354 if (offsetLeft < 0)
1355 this.actionBoxMenu.classList.add('left-edge-offset');
1356 else if (offsetLeft + menuWidth > window.innerWidth)
1357 this.actionBoxMenu.classList.add('right-edge-offset');
1358 } else {
1359 this.actionBoxAreaElement.classList.remove('active');
1360 this.actionBoxAreaElement.classList.remove('menu-moved-up');
1361 this.actionBoxMenu.classList.remove('menu-moved-up');
1362 }
1363 },
1364
1365 /**
1366 * Whether action box button is in hovered state.
1367 * @type {boolean}
1368 */
1369 get isActionBoxMenuHovered() {
1370 return this.actionBoxAreaElement.classList.contains('hovered');
1371 },
1372 set isActionBoxMenuHovered(hovered) {
1373 if (hovered == this.isActionBoxMenuHovered)
1374 return;
1375
1376 if (hovered) {
1377 this.actionBoxAreaElement.classList.add('hovered');
1378 this.classList.add('hovered');
1379 } else {
1380 if (this.multiProfilesPolicyApplied)
1381 this.userTypeBubbleElement.classList.remove('bubble-shown');
1382 this.actionBoxAreaElement.classList.remove('hovered');
1383 this.classList.remove('hovered');
1384 }
1385 },
1386
1387 /**
1388 * Set the authentication type for the pod.
1389 * @param {number} An auth type value defined in the AUTH_TYPE enum.
1390 * @param {string} authValue The initial value used for the auth type.
1391 */
1392 setAuthType: function(authType, authValue) {
1393 this.authType_ = authType;
1394 this.authValue_ = authValue;
1395 this.setAttribute('auth-type', AUTH_TYPE_NAMES[this.authType_]);
1396 this.update();
1397 this.reset(this.parentNode.isFocused(this));
1398 },
1399
1400 /**
1401 * The auth type of the user pod. This value is one of the enum
1402 * values in AUTH_TYPE.
1403 * @type {number}
1404 */
1405 get authType() {
1406 return this.authType_;
1407 },
1408
1409 /**
1410 * The initial value used for the pod's authentication type.
1411 * eg. a prepopulated password input when using password authentication.
1412 */
1413 get authValue() {
1414 return this.authValue_;
1415 },
1416
1417 /**
1418 * True if the the user pod uses a password to authenticate.
1419 * @type {bool}
1420 */
1421 get isAuthTypePassword() {
1422 return this.authType_ == AUTH_TYPE.OFFLINE_PASSWORD ||
1423 this.authType_ == AUTH_TYPE.FORCE_OFFLINE_PASSWORD;
1424 },
1425
1426 /**
1427 * True if the the user pod uses a user click to authenticate.
1428 * @type {bool}
1429 */
1430 get isAuthTypeUserClick() {
1431 return this.authType_ == AUTH_TYPE.USER_CLICK;
1432 },
1433
1434 /**
1435 * True if the the user pod uses a online sign in to authenticate.
1436 * @type {bool}
1437 */
1438 get isAuthTypeOnlineSignIn() {
1439 return this.authType_ == AUTH_TYPE.ONLINE_SIGN_IN;
1440 },
1441
1442 /**
1443 * Updates the image element of the user.
1444 */
1445 updateUserImage: function() {
1446 UserPod.userImageSalt_[this.user.username] = new Date().getTime();
1447 this.update();
1448 },
1449
1450 /**
1451 * Focuses on input element.
1452 */
1453 focusInput: function() {
1454 // Move tabIndex from the whole pod to the main input.
1455 // Note: the |mainInput| can be the pod itself.
1456 this.tabIndex = -1;
1457 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
1458 this.mainInput.focus();
1459 },
1460
1461 /**
1462 * Activates the pod.
1463 * @param {Event} e Event object.
1464 * @return {boolean} True if activated successfully.
1465 */
1466 activate: function(e) {
1467 if (this.isAuthTypeOnlineSignIn) {
1468 this.showSigninUI();
1469 } else if (this.isAuthTypeUserClick) {
1470 Oobe.disableSigninUI();
1471 this.classList.toggle('signing-in', true);
1472 chrome.send('attemptUnlock', [this.user.username]);
1473 } else if (this.isAuthTypePassword) {
1474 if (this.fingerprintAuthenticated_) {
1475 this.fingerprintAuthenticated_ = false;
1476 return true;
1477 }
1478 var pinValue = this.pinKeyboard ? this.pinKeyboard.value : '';
1479 var password = this.passwordElement.value || pinValue;
1480 if (!password)
1481 return false;
1482 Oobe.disableSigninUI();
1483 chrome.send('authenticateUser', [this.user.username, password,
1484 this.isPinShown()]);
1485 } else {
1486 console.error('Activating user pod with invalid authentication type: ' +
1487 this.authType);
1488 }
1489
1490 return true;
1491 },
1492
1493 showSupervisedUserSigninWarning: function() {
1494 // Legacy supervised user token has been invalidated.
1495 // Make sure that pod is focused i.e. "Sign in" button is seen.
1496 this.parentNode.focusPod(this);
1497
1498 var error = document.createElement('div');
1499 var messageDiv = document.createElement('div');
1500 messageDiv.className = 'error-message-bubble';
1501 messageDiv.textContent =
1502 loadTimeData.getString('supervisedUserExpiredTokenWarning');
1503 error.appendChild(messageDiv);
1504
1505 $('bubble').showContentForElement(
1506 this.reauthWarningElement,
1507 cr.ui.Bubble.Attachment.TOP,
1508 error,
1509 this.reauthWarningElement.offsetWidth / 2,
1510 4);
1511 // Move warning bubble up if it overlaps the shelf.
1512 var maxHeight =
1513 cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping($('bubble'));
1514 if (maxHeight < $('bubble').offsetHeight) {
1515 $('bubble').showContentForElement(
1516 this.reauthWarningElement,
1517 cr.ui.Bubble.Attachment.BOTTOM,
1518 error,
1519 this.reauthWarningElement.offsetWidth / 2,
1520 4);
1521 }
1522 },
1523
1524 /**
1525 * Shows signin UI for this user.
1526 */
1527 showSigninUI: function() {
1528 if (this.user.legacySupervisedUser && !this.user.isDesktopUser) {
1529 this.showSupervisedUserSigninWarning();
1530 } else {
1531 // Special case for multi-profiles sign in. We show users even if they
1532 // are not allowed per policy. Restrict those users from starting GAIA.
1533 if (this.multiProfilesPolicyApplied)
1534 return;
1535
1536 this.parentNode.showSigninUI(this.user.emailAddress);
1537 }
1538 },
1539
1540 /**
1541 * Resets the input field and updates the tab order of pod controls.
1542 * @param {boolean} takeFocus If true, input field takes focus.
1543 */
1544 reset: function(takeFocus) {
1545 this.passwordElement.value = '';
1546 if (this.pinKeyboard)
1547 this.pinKeyboard.value = '';
1548 this.updateInput_();
1549 this.classList.toggle('signing-in', false);
1550 if (takeFocus) {
1551 if (!this.multiProfilesPolicyApplied)
1552 this.focusInput(); // This will set a custom tab order.
1553 }
1554 else
1555 this.resetTabOrder();
1556 },
1557
1558 /**
1559 * Removes a user using the correct identifier based on user type.
1560 * @param {Object} user User to be removed.
1561 */
1562 removeUser: function(user) {
1563 chrome.send('removeUser',
1564 [user.isDesktopUser ? user.profilePath : user.username]);
1565 },
1566
1567 /**
1568 * Handles a click event on action area button.
1569 * @param {Event} e Click event.
1570 */
1571 handleActionAreaButtonClick_: function(e) {
1572 if (this.parentNode.disabled)
1573 return;
1574 this.isActionBoxMenuActive = !this.isActionBoxMenuActive;
1575 e.stopPropagation();
1576 },
1577
1578 /**
1579 * Handles a keydown event on action area button.
1580 * @param {Event} e KeyDown event.
1581 */
1582 handleActionAreaButtonKeyDown_: function(e) {
1583 if (this.disabled)
1584 return;
1585 switch (e.key) {
1586 case 'Enter':
1587 case ' ':
1588 if (this.parentNode.focusedPod_ && !this.isActionBoxMenuActive)
1589 this.isActionBoxMenuActive = true;
1590 e.stopPropagation();
1591 break;
1592 case 'ArrowUp':
1593 case 'ArrowDown':
1594 if (this.isActionBoxMenuActive) {
1595 this.actionBoxMenuRemoveElement.tabIndex =
1596 UserPodTabOrder.POD_MENU_ITEM;
1597 this.actionBoxMenuRemoveElement.focus();
1598 }
1599 e.stopPropagation();
1600 break;
1601 // Ignore these two, so ChromeVox hotkeys don't close the menu before
1602 // they can navigate through it.
1603 case 'Shift':
1604 case 'Meta':
1605 break;
1606 case 'Escape':
1607 this.actionBoxAreaElement.focus();
1608 this.isActionBoxMenuActive = false;
1609 e.stopPropagation();
1610 break;
1611 case 'Tab':
1612 if (!this.parentNode.alwaysFocusSinglePod)
1613 this.parentNode.focusPod();
1614 default:
1615 this.isActionBoxMenuActive = false;
1616 break;
1617 }
1618 },
1619
1620 /**
1621 * Handles a keydown event on menu title.
1622 * @param {Event} e KeyDown event.
1623 */
1624 handleMenuTitleElementKeyDown_: function(e) {
1625 if (this.disabled)
1626 return;
1627
1628 if (e.key != 'Tab') {
1629 this.handleActionAreaButtonKeyDown_(e);
1630 return;
1631 }
1632
1633 if (e.shiftKey == false) {
1634 if (this.actionBoxMenuRemoveElement.hidden) {
1635 this.isActionBoxMenuActive = false;
1636 } else {
1637 this.actionBoxMenuRemoveElement.tabIndex =
1638 UserPodTabOrder.POD_MENU_ITEM;
1639 this.actionBoxMenuRemoveElement.focus();
1640 e.preventDefault();
1641 }
1642 } else {
1643 this.isActionBoxMenuActive = false;
1644 this.focusInput();
1645 e.preventDefault();
1646 }
1647 },
1648
1649 /**
1650 * Handles a blur event on menu title.
1651 * @param {Event} e Blur event.
1652 */
1653 handleMenuTitleElementBlur_: function(e) {
1654 if (this.disabled)
1655 return;
1656 this.actionBoxMenuTitleElement.tabIndex = -1;
1657 },
1658
1659 /**
1660 * Handles a click event on remove user command.
1661 * @param {Event} e Click event.
1662 */
1663 handleRemoveCommandClick_: function(e) {
1664 this.showRemoveWarning_();
1665 },
1666
1667 /**
1668 * Move the action box menu up if needed.
1669 */
1670 moveActionMenuUpIfNeeded_: function() {
1671 // Skip checking (computationally expensive) if already moved up.
1672 if (this.actionBoxMenu.classList.contains('menu-moved-up'))
1673 return;
1674
1675 // Move up the menu if it overlaps shelf.
1676 var maxHeight = cr.ui.LoginUITools.getMaxHeightBeforeShelfOverlapping(
1677 this.actionBoxMenu, true);
1678 var actualHeight = parseInt(
1679 window.getComputedStyle(this.actionBoxMenu).height);
1680 if (maxHeight < actualHeight) {
1681 this.actionBoxMenu.classList.add('menu-moved-up');
1682 this.actionBoxAreaElement.classList.add('menu-moved-up');
1683 }
1684 },
1685
1686 /**
1687 * Shows remove user warning. Used for legacy supervised users
1688 * and non-device-owner on CrOS, and for all users on desktop.
1689 */
1690 showRemoveWarning_: function() {
1691 this.actionBoxMenuRemoveElement.hidden = true;
1692 this.actionBoxRemoveUserWarningElement.hidden = false;
1693
1694 if (!this.user.isDesktopUser) {
1695 this.moveActionMenuUpIfNeeded_();
1696 if (!this.user.legacySupervisedUser) {
1697 this.querySelector(
1698 '.action-box-remove-user-warning-text').style.display = 'none';
1699 this.querySelector(
1700 '.action-box-remove-user-warning-table-nonsync').style.display
1701 = 'none';
1702 var message = loadTimeData.getString('removeNonOwnerUserWarningText');
1703 this.updateRemoveNonOwnerUserWarningMessage_(this.user.profilePath,
1704 message);
1705 }
1706 } else {
1707 // Show extra statistics information for desktop users
1708 this.querySelector(
1709 '.action-box-remove-non-owner-user-warning-text').hidden = true;
1710 this.RemoveWarningDialogSetMessage_(true, false);
1711 // set a global handler for the callback
1712 window.updateRemoveWarningDialog =
1713 this.updateRemoveWarningDialog_.bind(this);
1714 chrome.send('removeUserWarningLoadStats', [this.user.profilePath]);
1715 }
1716 chrome.send('logRemoveUserWarningShown');
1717 },
1718
1719 /**
1720 * Refresh the statistics in the remove user warning dialog.
1721 * @param {string} profilePath The filepath of the URL (must be verified).
1722 * @param {Object} profileStats Statistics associated with profileURL.
1723 */
1724 updateRemoveWarningDialog_: function(profilePath, profileStats) {
1725 if (profilePath !== this.user.profilePath)
1726 return;
1727
1728 var stats_elements = this.statsMapElements;
1729 // Update individual statistics
1730 var hasErrors = false;
1731 for (var key in profileStats) {
1732 if (stats_elements.hasOwnProperty(key)) {
1733 if (profileStats[key].success) {
1734 this.user.statistics[key] = profileStats[key];
1735 } else if (!this.user.statistics[key].success) {
1736 hasErrors = true;
1737 stats_elements[key].textContent = '';
1738 }
1739 }
1740 }
1741
1742 this.RemoveWarningDialogSetMessage_(false, hasErrors);
1743 },
1744
1745 /**
1746 * Set the new message in the dialog.
1747 * @param {boolean} Whether this is the first output, that requires setting
1748 * a in-progress message.
1749 * @param {boolean} Whether any actual query to the statistics have failed.
1750 * Should be true only if there is an error and the corresponding statistic
1751 * is also unavailable in ProfileAttributesStorage.
1752 */
1753 RemoveWarningDialogSetMessage_: function(isInitial, hasErrors) {
1754 var stats_elements = this.statsMapElements;
1755 var total_count = 0;
1756 var num_stats_loaded = 0;
1757 for (var key in stats_elements) {
1758 if (this.user.statistics[key].success) {
1759 var count = this.user.statistics[key].count;
1760 stats_elements[key].textContent = count;
1761 total_count += count;
1762 num_stats_loaded++;
1763 }
1764 }
1765
1766 // this.classList is used for selecting the appropriate dialog.
1767 if (total_count)
1768 this.classList.remove('has-no-stats');
1769
1770 var is_synced_user = this.user.emailAddress !== "";
1771 // Write total number if all statistics are loaded.
1772 if (num_stats_loaded === Object.keys(stats_elements).length) {
1773 if (!total_count) {
1774 this.classList.add('has-no-stats');
1775 var message = loadTimeData.getString(
1776 is_synced_user ? 'removeUserWarningTextSyncNoStats' :
1777 'removeUserWarningTextNonSyncNoStats');
1778 this.updateRemoveWarningDialogSetMessage_(this.user.profilePath,
1779 message);
1780 } else {
1781 window.updateRemoveWarningDialogSetMessage =
1782 this.updateRemoveWarningDialogSetMessage_.bind(this);
1783 chrome.send('getRemoveWarningDialogMessage',[{
1784 profilePath: this.user.profilePath,
1785 isSyncedUser: is_synced_user,
1786 hasErrors: hasErrors,
1787 totalCount: total_count
1788 }]);
1789 }
1790 } else if (isInitial) {
1791 if (!this.user.isProfileLoaded) {
1792 message = loadTimeData.getString(
1793 is_synced_user ? 'removeUserWarningTextSyncNoStats' :
1794 'removeUserWarningTextNonSyncNoStats');
1795 this.updateRemoveWarningDialogSetMessage_(this.user.profilePath,
1796 message);
1797 } else {
1798 message = loadTimeData.getString(
1799 is_synced_user ? 'removeUserWarningTextSyncCalculating' :
1800 'removeUserWarningTextNonSyncCalculating');
1801 substitute = loadTimeData.getString(
1802 'removeUserWarningTextCalculating');
1803 this.updateRemoveWarningDialogSetMessage_(this.user.profilePath,
1804 message, substitute);
1805 }
1806 }
1807 },
1808
1809 /**
1810 * Refresh the message in the remove user warning dialog.
1811 * @param {string} profilePath The filepath of the URL (must be verified).
1812 * @param {string} message The message to be written.
1813 * @param {number|string=} count The number or string to replace $1 in
1814 * |message|. Can be omitted if $1 is not present in |message|.
1815 */
1816 updateRemoveWarningDialogSetMessage_: function(profilePath, message,
1817 count) {
1818 if (profilePath !== this.user.profilePath)
1819 return;
1820 // Add localized messages where $1 will be replaced with
1821 // <span class="total-count"></span> and $2 will be replaced with
1822 // <span class="email"></span>.
1823 var element = this.querySelector('.action-box-remove-user-warning-text');
1824 element.textContent = '';
1825
1826 messageParts = message.split(/(\$[12])/);
1827 var numParts = messageParts.length;
1828 for (var j = 0; j < numParts; j++) {
1829 if (messageParts[j] === '$1') {
1830 var elementToAdd = document.createElement('span');
1831 elementToAdd.classList.add('total-count');
1832 elementToAdd.textContent = count;
1833 element.appendChild(elementToAdd);
1834 } else if (messageParts[j] === '$2') {
1835 var elementToAdd = document.createElement('span');
1836 elementToAdd.classList.add('email');
1837 elementToAdd.textContent = this.user.emailAddress;
1838 element.appendChild(elementToAdd);
1839 } else {
1840 element.appendChild(document.createTextNode(messageParts[j]));
1841 }
1842 }
1843 this.moveActionMenuUpIfNeeded_();
1844 },
1845
1846 /**
1847 * Update the message in the "remove non-owner user warning" dialog on CrOS.
1848 * @param {string} profilePath The filepath of the URL (must be verified).
1849 * @param (string) message The message to be written.
1850 */
1851 updateRemoveNonOwnerUserWarningMessage_: function(profilePath, message) {
1852 if (profilePath !== this.user.profilePath)
1853 return;
1854 // Add localized messages where $1 will be replaced with
1855 // <span class="email"></span>.
1856 var element = this.querySelector(
1857 '.action-box-remove-non-owner-user-warning-text');
1858 element.textContent = '';
1859
1860 messageParts = message.split(/(\$[1])/);
1861 var numParts = messageParts.length;
1862 for (var j = 0; j < numParts; j++) {
1863 if (messageParts[j] == '$1') {
1864 var elementToAdd = document.createElement('span');
1865 elementToAdd.classList.add('email');
1866 elementToAdd.textContent = this.user.emailAddress;
1867 element.appendChild(elementToAdd);
1868 } else {
1869 element.appendChild(document.createTextNode(messageParts[j]));
1870 }
1871 }
1872 this.moveActionMenuUpIfNeeded_();
1873 },
1874
1875 /**
1876 * Handles a click event on remove user confirmation button.
1877 * @param {Event} e Click event.
1878 */
1879 handleRemoveUserConfirmationClick_: function(e) {
1880 if (this.isActionBoxMenuActive) {
1881 this.isActionBoxMenuActive = false;
1882 this.removeUser(this.user);
1883 e.stopPropagation();
1884 }
1885 },
1886
1887 /**
1888 * Handles mouseover event on fingerprint icon.
1889 * @param {Event} e MouseOver event.
1890 */
1891 handleFingerprintIconMouseOver_: function(e) {
1892 var bubbleContent = document.createElement('div');
1893 bubbleContent.textContent =
1894 loadTimeData.getString('fingerprintIconMessage');
1895 this.passwordElement.placeholder =
1896 loadTimeData.getString('fingerprintHint');
1897
1898 /** @const */ var BUBBLE_OFFSET = 25;
1899 /** @const */ var BUBBLE_PADDING = -8;
1900 var attachment = this.isPinShown() ? cr.ui.Bubble.Attachment.RIGHT :
1901 cr.ui.Bubble.Attachment.BOTTOM;
1902 var bubbleAnchor = this.getBubbleAnchorForFingerprintIcon_();
1903 $('bubble').showContentForElement(
1904 bubbleAnchor, attachment, bubbleContent, BUBBLE_OFFSET,
1905 BUBBLE_PADDING, true);
1906 },
1907
1908 /**
1909 * Handles mouseout event on fingerprint icon.
1910 * @param {Event} e MouseOut event.
1911 */
1912 handleFingerprintIconMouseOut_: function(e) {
1913 var bubbleAnchor = this.getBubbleAnchorForFingerprintIcon_();
1914 $('bubble').hideForElement(bubbleAnchor);
1915 this.passwordElement.placeholder = loadTimeData.getString(
1916 this.isPinShown() ? 'pinKeyboardPlaceholderPinPassword' :
1917 'passwordHint');
1918 },
1919
1920 /**
1921 * Returns bubble anchor of the fingerprint icon.
1922 * @return {!HTMLElement} Anchor element of the bubble.
1923 */
1924 getBubbleAnchorForFingerprintIcon_: function() {
1925 var bubbleAnchor = this;
1926 if (this.isPinShown())
1927 bubbleAnchor = (this.getElementsByClassName('auth-container'))[0];
1928 return bubbleAnchor;
1929 },
1930
1931 /**
1932 * Handles a keydown event on remove user confirmation button.
1933 * @param {Event} e KeyDown event.
1934 */
1935 handleRemoveUserConfirmationKeyDown_: function(e) {
1936 if (!this.isActionBoxMenuActive)
1937 return;
1938
1939 // Only handle pressing 'Enter' or 'Space', and let all other events
1940 // bubble to the action box menu.
1941 if (e.key == 'Enter' || e.key == ' ') {
1942 this.isActionBoxMenuActive = false;
1943 this.removeUser(this.user);
1944 e.stopPropagation();
1945 // Prevent default so that we don't trigger a 'click' event.
1946 e.preventDefault();
1947 }
1948 },
1949
1950 /**
1951 * Handles a keydown event on remove command.
1952 * @param {Event} e KeyDown event.
1953 */
1954 handleRemoveCommandKeyDown_: function(e) {
1955 if (this.disabled)
1956 return;
1957 switch (e.key) {
1958 case 'Enter':
1959 e.preventDefault();
1960 this.showRemoveWarning_();
1961 e.stopPropagation();
1962 break;
1963 case 'ArrowUp':
1964 case 'ArrowDown':
1965 e.stopPropagation();
1966 break;
1967 // Ignore these two, so ChromeVox hotkeys don't close the menu before
1968 // they can navigate through it.
1969 case 'Shift':
1970 case 'Meta':
1971 break;
1972 case 'Escape':
1973 this.actionBoxAreaElement.focus();
1974 this.isActionBoxMenuActive = false;
1975 e.stopPropagation();
1976 break;
1977 default:
1978 this.actionBoxAreaElement.focus();
1979 this.isActionBoxMenuActive = false;
1980 break;
1981 }
1982 },
1983
1984 /**
1985 * Handles a blur event on remove command.
1986 * @param {Event} e Blur event.
1987 */
1988 handleRemoveCommandBlur_: function(e) {
1989 if (this.disabled)
1990 return;
1991 this.actionBoxMenuRemoveElement.tabIndex = -1;
1992 },
1993
1994 /**
1995 * Handles mouse down event. It sets whether the user click auth will be
1996 * allowed on the next mouse click event. The auth is allowed iff the pod
1997 * was focused on the mouse down event starting the click.
1998 * @param {Event} e The mouse down event.
1999 */
2000 handlePodMouseDown_: function(e) {
2001 this.userClickAuthAllowed_ = this.parentNode.isFocused(this);
2002 },
2003
2004 /**
2005 * Called when the input of the password element changes. Updates the submit
2006 * button color and state and hides the error popup bubble.
2007 */
2008 updateInput_: function() {
2009 if (this.submitButton) {
2010 this.submitButton.disabled = this.passwordElement.value.length == 0;
2011 if (this.isFingerprintIconShown()) {
2012 this.submitButton.hidden = this.passwordElement.value.length == 0;
2013 } else {
2014 this.submitButton.hidden = false;
2015 }
2016 }
2017 this.showError = false;
2018 $('bubble').hide();
2019 },
2020
2021 /**
2022 * Handles input event on the password element.
2023 * @param {Event} e Input event.
2024 */
2025 handleInputChanged_: function(e) {
2026 this.updateInput_();
2027 },
2028
2029 /**
2030 * Handles mouse up event on the password element.
2031 * @param {Event} e Mouse up event.
2032 */
2033 handleInputMouseUp_: function(e) {
2034 // If the PIN keyboard is shown and the user clicks on the password
2035 // element, the virtual keyboard should pop up if it is enabled, so we
2036 // must disable the virtual keyboard override.
2037 if (this.isPinShown()) {
2038 chrome.send('setForceDisableVirtualKeyboard', [false]);
2039 }
2040 },
2041
2042 /**
2043 * Handles click event on a user pod.
2044 * @param {Event} e Click event.
2045 */
2046 handleClickOnPod_: function(e) {
2047 if (this.parentNode.disabled)
2048 return;
2049
2050 if (!this.isActionBoxMenuActive) {
2051 if (this.isAuthTypeOnlineSignIn) {
2052 this.showSigninUI();
2053 } else if (this.isAuthTypeUserClick && this.userClickAuthAllowed_) {
2054 // Note that this.userClickAuthAllowed_ is set in mouse down event
2055 // handler.
2056 this.parentNode.setActivatedPod(this);
2057 } else if (this.pinKeyboard &&
2058 e.target == this.pinKeyboard.submitButton) {
2059 // Sets the pod as activated if the submit button is clicked so that
2060 // it simulates what the enter button does for the password/pin.
2061 this.parentNode.setActivatedPod(this);
2062 }
2063
2064 if (this.multiProfilesPolicyApplied)
2065 this.userTypeBubbleElement.classList.add('bubble-shown');
2066
2067 // Prevent default so that we don't trigger 'focus' event and
2068 // stop propagation so that the 'click' event does not bubble
2069 // up and accidentally closes the bubble tooltip.
2070 stopEventPropagation(e);
2071 }
2072 },
2073
2074 /**
2075 * Handles keydown event for a user pod.
2076 * @param {Event} e Key event.
2077 */
2078 handlePodKeyDown_: function(e) {
2079 if (!this.isAuthTypeUserClick || this.disabled)
2080 return;
2081 switch (e.key) {
2082 case 'Enter':
2083 case ' ':
2084 if (this.parentNode.isFocused(this))
2085 this.parentNode.setActivatedPod(this);
2086 break;
2087 }
2088 }
2089 };
2090
2091 /**
2092 * Creates a public account user pod.
2093 * @constructor
2094 * @extends {UserPod}
2095 */
2096 var PublicAccountUserPod = cr.ui.define(function() {
2097 var node = UserPod();
2098
2099 var extras = $('public-account-user-pod-extras-template').children;
2100 for (var i = 0; i < extras.length; ++i) {
2101 var el = extras[i].cloneNode(true);
2102 node.appendChild(el);
2103 }
2104
2105 return node;
2106 });
2107
2108 PublicAccountUserPod.prototype = {
2109 __proto__: UserPod.prototype,
2110
2111 /**
2112 * "Enter" button in expanded side pane.
2113 * @type {!HTMLButtonElement}
2114 */
2115 get enterButtonElement() {
2116 return this.querySelector('.enter-button');
2117 },
2118
2119 /**
2120 * Boolean flag of whether the pod is showing the side pane. The flag
2121 * controls whether 'expanded' class is added to the pod's class list and
2122 * resets tab order because main input element changes when the 'expanded'
2123 * state changes.
2124 * @type {boolean}
2125 */
2126 get expanded() {
2127 return this.classList.contains('expanded');
2128 },
2129
2130 set expanded(expanded) {
2131 if (this.expanded == expanded)
2132 return;
2133
2134 this.resetTabOrder();
2135 this.classList.toggle('expanded', expanded);
2136 if (expanded) {
2137 // Show the advanced expanded pod directly if there are at least two
2138 // recommended locales. This will be the case in multilingual
2139 // environments where users are likely to want to choose among locales.
2140 if (this.querySelector('.language-select').multipleRecommendedLocales)
2141 this.classList.add('advanced');
2142 this.usualLeft = this.left;
2143 this.makeSpaceForExpandedPod_();
2144 } else if (typeof(this.usualLeft) != 'undefined') {
2145 this.left = this.usualLeft;
2146 }
2147
2148 var self = this;
2149 this.classList.add('animating');
2150 this.addEventListener('transitionend', function f(e) {
2151 self.removeEventListener('transitionend', f);
2152 self.classList.remove('animating');
2153
2154 // Accessibility focus indicator does not move with the focused
2155 // element. Sends a 'focus' event on the currently focused element
2156 // so that accessibility focus indicator updates its location.
2157 if (document.activeElement)
2158 document.activeElement.dispatchEvent(new Event('focus'));
2159 });
2160 // Guard timer set to animation duration + 20ms.
2161 ensureTransitionEndEvent(this, 200);
2162 },
2163
2164 get advanced() {
2165 return this.classList.contains('advanced');
2166 },
2167
2168 /** @override */
2169 get mainInput() {
2170 if (this.expanded)
2171 return this.enterButtonElement;
2172 else
2173 return this.nameElement;
2174 },
2175
2176 /** @override */
2177 decorate: function() {
2178 UserPod.prototype.decorate.call(this);
2179
2180 this.classList.add('public-account');
2181
2182 this.nameElement.addEventListener('keydown', (function(e) {
2183 if (e.key == 'Enter') {
2184 this.parentNode.setActivatedPod(this, e);
2185 // Stop this keydown event from bubbling up to PodRow handler.
2186 e.stopPropagation();
2187 // Prevent default so that we don't trigger a 'click' event on the
2188 // newly focused "Enter" button.
2189 e.preventDefault();
2190 }
2191 }).bind(this));
2192
2193 var learnMore = this.querySelector('.learn-more');
2194 learnMore.addEventListener('mousedown', stopEventPropagation);
2195 learnMore.addEventListener('click', this.handleLearnMoreEvent);
2196 learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
2197
2198 learnMore = this.querySelector('.expanded-pane-learn-more');
2199 learnMore.addEventListener('click', this.handleLearnMoreEvent);
2200 learnMore.addEventListener('keydown', this.handleLearnMoreEvent);
2201
2202 var languageSelect = this.querySelector('.language-select');
2203 languageSelect.tabIndex = UserPodTabOrder.POD_INPUT;
2204 languageSelect.manuallyChanged = false;
2205 languageSelect.addEventListener(
2206 'change',
2207 function() {
2208 languageSelect.manuallyChanged = true;
2209 this.getPublicSessionKeyboardLayouts_();
2210 }.bind(this));
2211
2212 var keyboardSelect = this.querySelector('.keyboard-select');
2213 keyboardSelect.tabIndex = UserPodTabOrder.POD_INPUT;
2214 keyboardSelect.loadedLocale = null;
2215
2216 var languageAndInput = this.querySelector('.language-and-input');
2217 languageAndInput.tabIndex = UserPodTabOrder.POD_INPUT;
2218 languageAndInput.addEventListener('click',
2219 this.transitionToAdvanced_.bind(this));
2220
2221 var monitoringLearnMore = this.querySelector('.monitoring-learn-more');
2222 monitoringLearnMore.tabIndex = UserPodTabOrder.POD_INPUT;
2223 monitoringLearnMore.addEventListener(
2224 'click', this.onMonitoringLearnMoreClicked_.bind(this));
2225
2226 this.enterButtonElement.addEventListener('click', (function(e) {
2227 this.enterButtonElement.disabled = true;
2228 var locale = this.querySelector('.language-select').value;
2229 var keyboardSelect = this.querySelector('.keyboard-select');
2230 // The contents of |keyboardSelect| is updated asynchronously. If its
2231 // locale does not match |locale|, it has not updated yet and the
2232 // currently selected keyboard layout may not be applicable to |locale|.
2233 // Do not return any keyboard layout in this case and let the backend
2234 // choose a suitable layout.
2235 var keyboardLayout =
2236 keyboardSelect.loadedLocale == locale ? keyboardSelect.value : '';
2237 chrome.send('launchPublicSession',
2238 [this.user.username, locale, keyboardLayout]);
2239 }).bind(this));
2240 },
2241
2242 /** @override **/
2243 initialize: function() {
2244 UserPod.prototype.initialize.call(this);
2245
2246 id = this.user.username + '-keyboard';
2247 this.querySelector('.keyboard-select-label').htmlFor = id;
2248 this.querySelector('.keyboard-select').setAttribute('id', id);
2249
2250 var id = this.user.username + '-language';
2251 this.querySelector('.language-select-label').htmlFor = id;
2252 var languageSelect = this.querySelector('.language-select');
2253 languageSelect.setAttribute('id', id);
2254 this.populateLanguageSelect(this.user.initialLocales,
2255 this.user.initialLocale,
2256 this.user.initialMultipleRecommendedLocales);
2257 },
2258
2259 /** @override **/
2260 update: function() {
2261 UserPod.prototype.update.call(this);
2262 this.querySelector('.expanded-pane-name').textContent =
2263 this.user_.displayName;
2264 this.querySelector('.info').textContent =
2265 loadTimeData.getStringF('publicAccountInfoFormat',
2266 this.user_.enterpriseDomain);
2267 },
2268
2269 /** @override */
2270 focusInput: function() {
2271 // Move tabIndex from the whole pod to the main input.
2272 this.tabIndex = -1;
2273 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
2274 this.mainInput.focus();
2275 },
2276
2277 /** @override */
2278 reset: function(takeFocus) {
2279 if (!takeFocus)
2280 this.expanded = false;
2281 this.enterButtonElement.disabled = false;
2282 UserPod.prototype.reset.call(this, takeFocus);
2283 },
2284
2285 /** @override */
2286 activate: function(e) {
2287 if (!this.expanded) {
2288 this.expanded = true;
2289 this.focusInput();
2290 }
2291 return true;
2292 },
2293
2294 /** @override */
2295 handleClickOnPod_: function(e) {
2296 if (this.parentNode.disabled)
2297 return;
2298
2299 this.parentNode.focusPod(this);
2300 this.parentNode.setActivatedPod(this, e);
2301 // Prevent default so that we don't trigger 'focus' event.
2302 e.preventDefault();
2303 },
2304
2305 /**
2306 * Updates the display name shown on the pod.
2307 * @param {string} displayName The new display name
2308 */
2309 setDisplayName: function(displayName) {
2310 this.user_.displayName = displayName;
2311 this.update();
2312 },
2313
2314 /**
2315 * Handle mouse and keyboard events for the learn more button. Triggering
2316 * the button causes information about public sessions to be shown.
2317 * @param {Event} event Mouse or keyboard event.
2318 */
2319 handleLearnMoreEvent: function(event) {
2320 switch (event.type) {
2321 // Show informaton on left click. Let any other clicks propagate.
2322 case 'click':
2323 if (event.button != 0)
2324 return;
2325 break;
2326 // Show informaton when <Return> or <Space> is pressed. Let any other
2327 // key presses propagate.
2328 case 'keydown':
2329 switch (event.keyCode) {
2330 case 13: // Return.
2331 case 32: // Space.
2332 break;
2333 default:
2334 return;
2335 }
2336 break;
2337 }
2338 chrome.send('launchHelpApp', [HELP_TOPIC_PUBLIC_SESSION]);
2339 stopEventPropagation(event);
2340 },
2341
2342 makeSpaceForExpandedPod_: function() {
2343 var width = this.classList.contains('advanced') ?
2344 PUBLIC_EXPANDED_ADVANCED_WIDTH : PUBLIC_EXPANDED_BASIC_WIDTH;
2345 var isDesktopUserManager = Oobe.getInstance().displayType ==
2346 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2347 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
2348 POD_ROW_PADDING;
2349 if (this.left + width > $('pod-row').offsetWidth - rowPadding)
2350 this.left = $('pod-row').offsetWidth - rowPadding - width;
2351 },
2352
2353 /**
2354 * Transition the expanded pod from the basic to the advanced view.
2355 */
2356 transitionToAdvanced_: function() {
2357 var pod = this;
2358 var languageAndInputSection =
2359 this.querySelector('.language-and-input-section');
2360 this.classList.add('transitioning-to-advanced');
2361 setTimeout(function() {
2362 pod.classList.add('advanced');
2363 pod.makeSpaceForExpandedPod_();
2364 languageAndInputSection.addEventListener('transitionend',
2365 function observer() {
2366 languageAndInputSection.removeEventListener('transitionend',
2367 observer);
2368 pod.classList.remove('transitioning-to-advanced');
2369 pod.querySelector('.language-select').focus();
2370 });
2371 // Guard timer set to animation duration + 20ms.
2372 ensureTransitionEndEvent(languageAndInputSection, 380);
2373 }, 0);
2374 },
2375
2376 /**
2377 * Show a dialog when user clicks on learn more (monitoring) button.
2378 */
2379 onMonitoringLearnMoreClicked_: function() {
2380 if (!this.dialogContainer_) {
2381 this.dialogContainer_ = document.createElement('div');
2382 this.dialogContainer_.classList.add('monitoring-dialog-container');
2383 var topContainer = document.querySelector('#scroll-container');
2384 topContainer.appendChild(this.dialogContainer_);
2385 }
2386 // Public Session POD in advanced view has a different size so add a dummy
2387 // parent element to enable different CSS settings.
2388 this.dialogContainer_.classList.toggle(
2389 'advanced', this.classList.contains('advanced'))
2390 var html = '';
2391 var infoItems = ['publicAccountMonitoringInfoItem1',
2392 'publicAccountMonitoringInfoItem2',
2393 'publicAccountMonitoringInfoItem3',
2394 'publicAccountMonitoringInfoItem4'];
2395 for (item of infoItems) {
2396 html += '<p class="cr-dialog-item">';
2397 html += loadTimeData.getString(item);
2398 html += '</p>';
2399 }
2400 var title = loadTimeData.getString('publicAccountMonitoringInfo');
2401 this.dialog_ = new cr.ui.dialogs.BaseDialog(this.dialogContainer_);
2402 this.dialog_.showHtml(title, html, undefined,
2403 this.onMonitoringDialogClosed_.bind(this));
2404 this.parentNode.disabled = true;
2405 },
2406
2407 /**
2408 * Cleanup after the monitoring warning dialog is closed.
2409 */
2410 onMonitoringDialogClosed_: function() {
2411 this.parentNode.disabled = false;
2412 this.dialog_ = undefined;
2413 },
2414
2415 /**
2416 * Retrieves the list of keyboard layouts available for the currently
2417 * selected locale.
2418 */
2419 getPublicSessionKeyboardLayouts_: function() {
2420 var selectedLocale = this.querySelector('.language-select').value;
2421 if (selectedLocale ==
2422 this.querySelector('.keyboard-select').loadedLocale) {
2423 // If the list of keyboard layouts was loaded for the currently selected
2424 // locale, it is already up to date.
2425 return;
2426 }
2427 chrome.send('getPublicSessionKeyboardLayouts',
2428 [this.user.username, selectedLocale]);
2429 },
2430
2431 /**
2432 * Populates the keyboard layout "select" element with a list of layouts.
2433 * @param {string} locale The locale to which this list of keyboard layouts
2434 * applies
2435 * @param {!Object} list List of available keyboard layouts
2436 */
2437 populateKeyboardSelect: function(locale, list) {
2438 if (locale != this.querySelector('.language-select').value) {
2439 // The selected locale has changed and the list of keyboard layouts is
2440 // not applicable. This method will be called again when a list of
2441 // keyboard layouts applicable to the selected locale is retrieved.
2442 return;
2443 }
2444
2445 var keyboardSelect = this.querySelector('.keyboard-select');
2446 keyboardSelect.loadedLocale = locale;
2447 keyboardSelect.innerHTML = '';
2448 for (var i = 0; i < list.length; ++i) {
2449 var item = list[i];
2450 keyboardSelect.appendChild(
2451 new Option(item.title, item.value, item.selected, item.selected));
2452 }
2453 },
2454
2455 /**
2456 * Populates the language "select" element with a list of locales.
2457 * @param {!Object} locales The list of available locales
2458 * @param {string} defaultLocale The locale to select by default
2459 * @param {boolean} multipleRecommendedLocales Whether |locales| contains
2460 * two or more recommended locales
2461 */
2462 populateLanguageSelect: function(locales,
2463 defaultLocale,
2464 multipleRecommendedLocales) {
2465 var languageSelect = this.querySelector('.language-select');
2466 // If the user manually selected a locale, do not change the selection.
2467 // Otherwise, select the new |defaultLocale|.
2468 var selected =
2469 languageSelect.manuallyChanged ? languageSelect.value : defaultLocale;
2470 languageSelect.innerHTML = '';
2471 var group = languageSelect;
2472 for (var i = 0; i < locales.length; ++i) {
2473 var item = locales[i];
2474 if (item.optionGroupName) {
2475 group = document.createElement('optgroup');
2476 group.label = item.optionGroupName;
2477 languageSelect.appendChild(group);
2478 } else {
2479 group.appendChild(new Option(item.title,
2480 item.value,
2481 item.value == selected,
2482 item.value == selected));
2483 }
2484 }
2485 languageSelect.multipleRecommendedLocales = multipleRecommendedLocales;
2486
2487 // Retrieve a list of keyboard layouts applicable to the locale that is
2488 // now selected.
2489 this.getPublicSessionKeyboardLayouts_();
2490 }
2491 };
2492
2493 /**
2494 * Creates a user pod to be used only in desktop chrome.
2495 * @constructor
2496 * @extends {UserPod}
2497 */
2498 var DesktopUserPod = cr.ui.define(function() {
2499 // Don't just instantiate a UserPod(), as this will call decorate() on the
2500 // parent object, and add duplicate event listeners.
2501 var node = $('user-pod-template').cloneNode(true);
2502 node.removeAttribute('id');
2503 return node;
2504 });
2505
2506 DesktopUserPod.prototype = {
2507 __proto__: UserPod.prototype,
2508
2509 /** @override */
2510 initialize: function() {
2511 if (this.user.needsSignin) {
2512 if (this.user.hasLocalCreds) {
2513 this.user.initialAuthType = AUTH_TYPE.OFFLINE_PASSWORD;
2514 } else {
2515 this.user.initialAuthType = AUTH_TYPE.ONLINE_SIGN_IN;
2516 }
2517 }
2518 UserPod.prototype.initialize.call(this);
2519 },
2520
2521 /** @override */
2522 get mainInput() {
2523 if (this.user.needsSignin)
2524 return this.passwordElement;
2525 else
2526 return this.nameElement;
2527 },
2528
2529 /** @override */
2530 update: function() {
2531 this.imageElement.src = this.user.userImage;
2532 this.nameElement.textContent = this.user.displayName;
2533 this.reauthNameHintElement.textContent = this.user.displayName;
2534
2535 var isLockedUser = this.user.needsSignin;
2536 var isLegacySupervisedUser = this.user.legacySupervisedUser;
2537 var isChildUser = this.user.childUser;
2538 var isSyncedUser = this.user.emailAddress !== "";
2539 var isProfileLoaded = this.user.isProfileLoaded;
2540 this.classList.toggle('locked', isLockedUser);
2541 this.classList.toggle('legacy-supervised', isLegacySupervisedUser);
2542 this.classList.toggle('child', isChildUser);
2543 this.classList.toggle('synced', isSyncedUser);
2544 this.classList.toggle('has-no-stats',
2545 !isProfileLoaded && !this.user.statistics.length);
2546
2547 if (this.isAuthTypeUserClick)
2548 this.passwordLabelElement.textContent = this.authValue;
2549
2550 this.passwordElement.setAttribute('aria-label', loadTimeData.getStringF(
2551 'passwordFieldAccessibleName', this.user_.emailAddress));
2552
2553 UserPod.prototype.updateActionBoxArea.call(this);
2554 },
2555
2556 /** @override */
2557 activate: function(e) {
2558 if (!this.user.needsSignin) {
2559 Oobe.launchUser(this.user.profilePath);
2560 } else if (this.user.hasLocalCreds && !this.passwordElement.value) {
2561 return false;
2562 } else {
2563 chrome.send('authenticatedLaunchUser',
2564 [this.user.profilePath,
2565 this.user.emailAddress,
2566 this.passwordElement.value]);
2567 }
2568 this.passwordElement.value = '';
2569 return true;
2570 },
2571
2572 /** @override */
2573 handleClickOnPod_: function(e) {
2574 if (this.parentNode.disabled)
2575 return;
2576
2577 Oobe.clearErrors();
2578 this.parentNode.lastFocusedPod_ = this;
2579
2580 // If this is a locked pod and there are local credentials, show the
2581 // password field. Otherwise call activate() which will open up a browser
2582 // window or show the reauth dialog, as needed.
2583 if (!(this.user.needsSignin && this.user.hasLocalCreds) &&
2584 !this.isActionBoxMenuActive) {
2585 this.activate(e);
2586 }
2587
2588 if (this.isAuthTypeUserClick)
2589 chrome.send('attemptUnlock', [this.user.emailAddress]);
2590 },
2591 };
2592
2593 /**
2594 * Creates a user pod that represents kiosk app.
2595 * @constructor
2596 * @extends {UserPod}
2597 */
2598 var KioskAppPod = cr.ui.define(function() {
2599 var node = UserPod();
2600 return node;
2601 });
2602
2603 KioskAppPod.prototype = {
2604 __proto__: UserPod.prototype,
2605
2606 /** @override */
2607 decorate: function() {
2608 UserPod.prototype.decorate.call(this);
2609 this.launchAppButtonElement.addEventListener('click',
2610 this.activate.bind(this));
2611 },
2612
2613 /** @override */
2614 update: function() {
2615 this.imageElement.src = this.user.iconUrl;
2616 this.imageElement.alt = this.user.label;
2617 this.imageElement.title = this.user.label;
2618 this.passwordEntryContainerElement.hidden = true;
2619 this.launchAppButtonContainerElement.hidden = false;
2620 this.nameElement.textContent = this.user.label;
2621 this.reauthNameHintElement.textContent = this.user.label;
2622
2623 UserPod.prototype.updateActionBoxArea.call(this);
2624 UserPod.prototype.customizeUserPodPerUserType.call(this);
2625 },
2626
2627 /** @override */
2628 get mainInput() {
2629 return this.launchAppButtonElement;
2630 },
2631
2632 /** @override */
2633 focusInput: function() {
2634 // Move tabIndex from the whole pod to the main input.
2635 this.tabIndex = -1;
2636 this.mainInput.tabIndex = UserPodTabOrder.POD_INPUT;
2637 this.mainInput.focus();
2638 },
2639
2640 /** @override */
2641 get forceOnlineSignin() {
2642 return false;
2643 },
2644
2645 /** @override */
2646 activate: function(e) {
2647 var diagnosticMode = e && e.ctrlKey;
2648 this.launchApp_(this.user, diagnosticMode);
2649 return true;
2650 },
2651
2652 /** @override */
2653 handleClickOnPod_: function(e) {
2654 if (this.parentNode.disabled)
2655 return;
2656
2657 Oobe.clearErrors();
2658 this.parentNode.lastFocusedPod_ = this;
2659 this.activate(e);
2660 },
2661
2662 /**
2663 * Launch the app. If |diagnosticMode| is true, ask user to confirm.
2664 * @param {Object} app App data.
2665 * @param {boolean} diagnosticMode Whether to run the app in diagnostic
2666 * mode.
2667 */
2668 launchApp_: function(app, diagnosticMode) {
2669 if (!diagnosticMode) {
2670 chrome.send('launchKioskApp', [app.id, false]);
2671 return;
2672 }
2673
2674 var oobe = $('oobe');
2675 if (!oobe.confirmDiagnosticMode_) {
2676 oobe.confirmDiagnosticMode_ =
2677 new cr.ui.dialogs.ConfirmDialog(document.body);
2678 oobe.confirmDiagnosticMode_.setOkLabel(
2679 loadTimeData.getString('confirmKioskAppDiagnosticModeYes'));
2680 oobe.confirmDiagnosticMode_.setCancelLabel(
2681 loadTimeData.getString('confirmKioskAppDiagnosticModeNo'));
2682 }
2683
2684 oobe.confirmDiagnosticMode_.show(
2685 loadTimeData.getStringF('confirmKioskAppDiagnosticModeFormat',
2686 app.label),
2687 function() {
2688 chrome.send('launchKioskApp', [app.id, true]);
2689 });
2690 },
2691 };
2692
2693 /**
2694 * Creates a new pod row element.
2695 * @constructor
2696 * @extends {HTMLDivElement}
2697 */
2698 var PodRow = cr.ui.define('podrow');
2699
2700 PodRow.prototype = {
2701 __proto__: HTMLDivElement.prototype,
2702
2703 // Whether this user pod row is shown for the first time.
2704 firstShown_: true,
2705
2706 // True if inside focusPod().
2707 insideFocusPod_: false,
2708
2709 // Focused pod.
2710 focusedPod_: undefined,
2711
2712 // Activated pod, i.e. the pod of current login attempt.
2713 activatedPod_: undefined,
2714
2715 // Pod that was most recently focused, if any.
2716 lastFocusedPod_: undefined,
2717
2718 // Pods whose initial images haven't been loaded yet.
2719 podsWithPendingImages_: [],
2720
2721 // Whether pod placement has been postponed.
2722 podPlacementPostponed_: false,
2723
2724 // Standard user pod height/width.
2725 userPodHeight_: 0,
2726 userPodWidth_: 0,
2727
2728 // Array of apps that are shown in addition to other user pods.
2729 apps_: [],
2730
2731 // True to show app pods along with user pods.
2732 shouldShowApps_: true,
2733
2734 // Array of users that are shown (public/supervised/regular).
2735 users_: [],
2736
2737 // If we're in Touch View mode.
2738 touchViewEnabled_: false,
2739
2740 /** @override */
2741 decorate: function() {
2742 // Event listeners that are installed for the time period during which
2743 // the element is visible.
2744 this.listeners_ = {
2745 focus: [this.handleFocus_.bind(this), true /* useCapture */],
2746 click: [this.handleClick_.bind(this), true],
2747 mousemove: [this.handleMouseMove_.bind(this), false],
2748 keydown: [this.handleKeyDown.bind(this), false]
2749 };
2750
2751 var isDesktopUserManager = Oobe.getInstance().displayType ==
2752 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2753 var isNewDesktopUserManager = Oobe.getInstance().newDesktopUserManager;
2754 this.userPodHeight_ = isDesktopUserManager ?
2755 isNewDesktopUserManager ? MD_DESKTOP_POD_HEIGHT :
2756 DESKTOP_POD_HEIGHT :
2757 CROS_POD_HEIGHT;
2758 this.userPodWidth_ = isDesktopUserManager ?
2759 isNewDesktopUserManager ? MD_DESKTOP_POD_WIDTH :
2760 DESKTOP_POD_WIDTH :
2761 CROS_POD_WIDTH;
2762 },
2763
2764 /**
2765 * Returns all the pods in this pod row.
2766 * @type {NodeList}
2767 */
2768 get pods() {
2769 return Array.prototype.slice.call(this.children);
2770 },
2771
2772 /**
2773 * Return true if user pod row has only single user pod in it, which should
2774 * always be focused except desktop and touch view modes.
2775 * @type {boolean}
2776 */
2777 get alwaysFocusSinglePod() {
2778 var isDesktopUserManager = Oobe.getInstance().displayType ==
2779 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
2780
2781 return (isDesktopUserManager || this.touchViewEnabled_) ?
2782 false : this.children.length == 1;
2783 },
2784
2785 /**
2786 * Returns pod with the given app id.
2787 * @param {!string} app_id Application id to be matched.
2788 * @return {Object} Pod with the given app id. null if pod hasn't been
2789 * found.
2790 */
2791 getPodWithAppId_: function(app_id) {
2792 for (var i = 0, pod; pod = this.pods[i]; ++i) {
2793 if (pod.user.isApp && pod.user.id == app_id)
2794 return pod;
2795 }
2796 return null;
2797 },
2798
2799 /**
2800 * Returns pod with the given username (null if there is no such pod).
2801 * @param {string} username Username to be matched.
2802 * @return {Object} Pod with the given username. null if pod hasn't been
2803 * found.
2804 */
2805 getPodWithUsername_: function(username) {
2806 for (var i = 0, pod; pod = this.pods[i]; ++i) {
2807 if (pod.user.username == username)
2808 return pod;
2809 }
2810 return null;
2811 },
2812
2813 /**
2814 * True if the the pod row is disabled (handles no user interaction).
2815 * @type {boolean}
2816 */
2817 disabled_: false,
2818 get disabled() {
2819 return this.disabled_;
2820 },
2821 set disabled(value) {
2822 this.disabled_ = value;
2823 this.pods.forEach(function(pod) {
2824 pod.disabled = value;
2825 });
2826 },
2827
2828 /**
2829 * Creates a user pod from given email.
2830 * @param {!Object} user User info dictionary.
2831 */
2832 createUserPod: function(user) {
2833 var userPod;
2834 if (user.isDesktopUser)
2835 userPod = new DesktopUserPod({user: user});
2836 else if (user.publicAccount)
2837 userPod = new PublicAccountUserPod({user: user});
2838 else if (user.isApp)
2839 userPod = new KioskAppPod({user: user});
2840 else
2841 userPod = new UserPod({user: user});
2842
2843 userPod.hidden = false;
2844 return userPod;
2845 },
2846
2847 /**
2848 * Add an existing user pod to this pod row.
2849 * @param {!Object} user User info dictionary.
2850 */
2851 addUserPod: function(user) {
2852 var userPod = this.createUserPod(user);
2853 this.appendChild(userPod);
2854 userPod.initialize();
2855 },
2856
2857 /**
2858 * Performs visual changes on the user pod if there is an error.
2859 * @param {boolean} visible Whether to show or hide the display.
2860 */
2861 setFocusedPodErrorDisplay: function(visible) {
2862 if (this.focusedPod_)
2863 this.focusedPod_.showError = visible;
2864 },
2865
2866 /**
2867 * Shows or hides the pin keyboard for the current focused pod.
2868 * @param {boolean} visible
2869 */
2870 setFocusedPodPinVisibility: function(visible) {
2871 if (this.focusedPod_)
2872 this.focusedPod_.setPinVisibility(visible);
2873 },
2874
2875 /**
2876 * Runs app with a given id from the list of loaded apps.
2877 * @param {!string} app_id of an app to run.
2878 * @param {boolean=} opt_diagnosticMode Whether to run the app in
2879 * diagnostic mode. Default is false.
2880 */
2881 findAndRunAppForTesting: function(app_id, opt_diagnosticMode) {
2882 var app = this.getPodWithAppId_(app_id);
2883 if (app) {
2884 var activationEvent = cr.doc.createEvent('MouseEvents');
2885 var ctrlKey = opt_diagnosticMode;
2886 activationEvent.initMouseEvent('click', true, true, null,
2887 0, 0, 0, 0, 0, ctrlKey, false, false, false, 0, null);
2888 app.dispatchEvent(activationEvent);
2889 }
2890 },
2891
2892 /**
2893 * Enables or disables the pin keyboard for the given user. A disabled pin
2894 * keyboard will never be displayed.
2895 *
2896 * If the user's pod is focused, then enabling the pin keyboard will display
2897 * it; disabling the pin keyboard will hide it.
2898 * @param {!string} username
2899 * @param {boolean} enabled
2900 */
2901 setPinEnabled: function(username, enabled) {
2902 var pod = this.getPodWithUsername_(username);
2903 if (!pod) {
2904 console.error('Attempt to enable/disable pin keyboard of missing pod.');
2905 return;
2906 }
2907
2908 // Make sure to set |pinEnabled| before toggling visiblity to avoid
2909 // validation errors.
2910 pod.pinEnabled = enabled;
2911
2912 if (this.focusedPod_ == pod) {
2913 if (enabled) {
2914 ensurePinKeyboardLoaded(
2915 this.setPinVisibility.bind(this, username, true));
2916 } else {
2917 this.setPinVisibility(username, false);
2918 }
2919 }
2920 },
2921
2922 /**
2923 * Shows or hides the pin keyboard from the pod with the given |username|.
2924 * This is only a visibility change; the pin keyboard can be reshown.
2925 *
2926 * Use setPinEnabled if the pin keyboard should be disabled for the given
2927 * user.
2928 * @param {!user} username
2929 * @param {boolean} visible
2930 */
2931 setPinVisibility: function(username, visible) {
2932 var pod = this.getPodWithUsername_(username);
2933 if (!pod) {
2934 console.error('Attempt to show/hide pin keyboard of missing pod.');
2935 return;
2936 }
2937 if (visible && pod.pinEnabled === false) {
2938 console.error('Attempt to show disabled pin keyboard');
2939 return;
2940 }
2941 if (visible && this.focusedPod_ != pod) {
2942 console.error('Attempt to show pin keyboard on non-focused pod');
2943 return;
2944 }
2945
2946 pod.setPinVisibility(visible);
2947 },
2948
2949 /**
2950 * Removes user pod from pod row.
2951 * @param {!user} username
2952 */
2953 removeUserPod: function(username) {
2954 var podToRemove = this.getPodWithUsername_(username);
2955 if (podToRemove == null) {
2956 console.warn('Attempt to remove pod that does not exist');
2957 return;
2958 }
2959 this.removeChild(podToRemove);
2960 if (this.pods.length > 0)
2961 this.placePods_();
2962 },
2963
2964 /**
2965 * Returns index of given pod or -1 if not found.
2966 * @param {UserPod} pod Pod to look up.
2967 * @private
2968 */
2969 indexOf_: function(pod) {
2970 for (var i = 0; i < this.pods.length; ++i) {
2971 if (pod == this.pods[i])
2972 return i;
2973 }
2974 return -1;
2975 },
2976
2977 /**
2978 * Populates pod row with given existing users and start init animation.
2979 * @param {array} users Array of existing user emails.
2980 */
2981 loadPods: function(users) {
2982 this.users_ = users;
2983
2984 this.rebuildPods();
2985 },
2986
2987 /**
2988 * Scrolls focused user pod into view.
2989 */
2990 scrollFocusedPodIntoView: function() {
2991 var pod = this.focusedPod_;
2992 if (!pod)
2993 return;
2994
2995 // First check whether focused pod is already fully visible.
2996 var visibleArea = $('scroll-container');
2997 // Visible area may not defined at user manager screen on all platforms.
2998 // Windows, Mac and Linux do not have visible area.
2999 if (!visibleArea)
3000 return;
3001 var scrollTop = visibleArea.scrollTop;
3002 var clientHeight = visibleArea.clientHeight;
3003 var podTop = $('oobe').offsetTop + pod.offsetTop;
3004 var padding = USER_POD_KEYBOARD_MIN_PADDING;
3005 if (podTop + pod.height + padding <= scrollTop + clientHeight &&
3006 podTop - padding >= scrollTop) {
3007 return;
3008 }
3009
3010 // Scroll so that user pod is as centered as possible.
3011 visibleArea.scrollTop = podTop - (clientHeight - pod.offsetHeight) / 2;
3012 },
3013
3014 /**
3015 * Rebuilds pod row using users_ and apps_ that were previously set or
3016 * updated.
3017 */
3018 rebuildPods: function() {
3019 var emptyPodRow = this.pods.length == 0;
3020
3021 // Clear existing pods.
3022 this.innerHTML = '';
3023 this.focusedPod_ = undefined;
3024 this.activatedPod_ = undefined;
3025 this.lastFocusedPod_ = undefined;
3026
3027 // Switch off animation
3028 Oobe.getInstance().toggleClass('flying-pods', false);
3029
3030 // Populate the pod row.
3031 for (var i = 0; i < this.users_.length; ++i)
3032 this.addUserPod(this.users_[i]);
3033
3034 for (var i = 0, pod; pod = this.pods[i]; ++i)
3035 this.podsWithPendingImages_.push(pod);
3036
3037 // TODO(nkostylev): Edge case handling when kiosk apps are not fitting.
3038 if (this.shouldShowApps_) {
3039 for (var i = 0; i < this.apps_.length; ++i)
3040 this.addUserPod(this.apps_[i]);
3041 }
3042
3043 // Make sure we eventually show the pod row, even if some image is stuck.
3044 setTimeout(function() {
3045 $('pod-row').classList.remove('images-loading');
3046 }, POD_ROW_IMAGES_LOAD_TIMEOUT_MS);
3047
3048 var isAccountPicker = $('login-header-bar').signinUIState ==
3049 SIGNIN_UI_STATE.ACCOUNT_PICKER;
3050
3051 // Immediately recalculate pods layout only when current UI is account
3052 // picker. Otherwise postpone it.
3053 if (isAccountPicker) {
3054 this.placePods_();
3055 this.maybePreselectPod();
3056
3057 // Without timeout changes in pods positions will be animated even
3058 // though it happened when 'flying-pods' class was disabled.
3059 setTimeout(function() {
3060 Oobe.getInstance().toggleClass('flying-pods', true);
3061 }, 0);
3062 } else {
3063 this.podPlacementPostponed_ = true;
3064
3065 // Update [Cancel] button state.
3066 if ($('login-header-bar').signinUIState ==
3067 SIGNIN_UI_STATE.GAIA_SIGNIN &&
3068 emptyPodRow &&
3069 this.pods.length > 0) {
3070 login.GaiaSigninScreen.updateControlsState();
3071 }
3072 }
3073 },
3074
3075 /**
3076 * Adds given apps to the pod row.
3077 * @param {array} apps Array of apps.
3078 */
3079 setApps: function(apps) {
3080 this.apps_ = apps;
3081 this.rebuildPods();
3082 chrome.send('kioskAppsLoaded');
3083
3084 // Check whether there's a pending kiosk app error.
3085 window.setTimeout(function() {
3086 chrome.send('checkKioskAppLaunchError');
3087 }, 500);
3088 },
3089
3090 /**
3091 * Sets whether should show app pods.
3092 * @param {boolean} shouldShowApps Whether app pods should be shown.
3093 */
3094 setShouldShowApps: function(shouldShowApps) {
3095 if (this.shouldShowApps_ == shouldShowApps)
3096 return;
3097
3098 this.shouldShowApps_ = shouldShowApps;
3099 this.rebuildPods();
3100 },
3101
3102 /**
3103 * Shows a custom icon on a user pod besides the input field.
3104 * @param {string} username Username of pod to add button
3105 * @param {!{id: !string,
3106 * hardlockOnClick: boolean,
3107 * isTrialRun: boolean,
3108 * ariaLabel: string | undefined,
3109 * tooltip: ({text: string, autoshow: boolean} | undefined)}} icon
3110 * The icon parameters.
3111 */
3112 showUserPodCustomIcon: function(username, icon) {
3113 var pod = this.getPodWithUsername_(username);
3114 if (pod == null) {
3115 console.error('Unable to show user pod button: user pod not found.');
3116 return;
3117 }
3118
3119 if (!icon.id && !icon.tooltip)
3120 return;
3121
3122 if (icon.id)
3123 pod.customIconElement.setIcon(icon.id);
3124
3125 if (icon.isTrialRun) {
3126 pod.customIconElement.setInteractive(
3127 this.onDidClickLockIconDuringTrialRun_.bind(this, username));
3128 } else if (icon.hardlockOnClick) {
3129 pod.customIconElement.setInteractive(
3130 this.hardlockUserPod_.bind(this, username));
3131 } else {
3132 pod.customIconElement.setInteractive(null);
3133 }
3134
3135 var ariaLabel = icon.ariaLabel || (icon.tooltip && icon.tooltip.text);
3136 if (ariaLabel)
3137 pod.customIconElement.setAriaLabel(ariaLabel);
3138 else
3139 console.warn('No ARIA label for user pod custom icon.');
3140
3141 pod.customIconElement.show();
3142
3143 // This has to be called after |show| in case the tooltip should be shown
3144 // immediatelly.
3145 pod.customIconElement.setTooltip(
3146 icon.tooltip || {text: '', autoshow: false});
3147
3148 // Hide fingerprint icon when custom icon is shown.
3149 this.setUserPodFingerprintIcon(username, FINGERPRINT_STATES.HIDDEN);
3150 },
3151
3152 /**
3153 * Hard-locks user pod for the user. If user pod is hard-locked, it can be
3154 * only unlocked using password, and the authentication type cannot be
3155 * changed.
3156 * @param {!string} username The user's username.
3157 * @private
3158 */
3159 hardlockUserPod_: function(username) {
3160 chrome.send('hardlockPod', [username]);
3161 },
3162
3163 /**
3164 * Records a metric indicating that the user clicked on the lock icon during
3165 * the trial run for Easy Unlock.
3166 * @param {!string} username The user's username.
3167 * @private
3168 */
3169 onDidClickLockIconDuringTrialRun_: function(username) {
3170 chrome.send('recordClickOnLockIcon', [username]);
3171 },
3172
3173 /**
3174 * Hides the custom icon in the user pod added by showUserPodCustomIcon().
3175 * @param {string} username Username of pod to remove button
3176 */
3177 hideUserPodCustomIcon: function(username) {
3178 var pod = this.getPodWithUsername_(username);
3179 if (pod == null) {
3180 console.error('Unable to hide user pod button: user pod not found.');
3181 return;
3182 }
3183
3184 // TODO(tengs): Allow option for a fading transition.
3185 pod.customIconElement.hide();
3186
3187 // Show fingerprint icon if applicable.
3188 this.setUserPodFingerprintIcon(username, FINGERPRINT_STATES.DEFAULT);
3189 },
3190
3191 /**
3192 * Set a fingerprint icon in the user pod of |username|.
3193 * @param {string} username Username of the selected user
3194 * @param {number} state Fingerprint unlock state
3195 */
3196 setUserPodFingerprintIcon: function(username, state) {
3197 var pod = this.getPodWithUsername_(username);
3198 if (pod == null) {
3199 console.error(
3200 'Unable to set user pod fingerprint icon: user pod not found.');
3201 return;
3202 }
3203 pod.fingerprintAuthenticated_ = false;
3204 if (!pod.fingerprintIconElement)
3205 return;
3206 if (!pod.user.allowFingerprint || state == FINGERPRINT_STATES.HIDDEN ||
3207 !pod.customIconElement.hidden) {
3208 pod.fingerprintIconElement.hidden = true;
3209 pod.submitButton.hidden = false;
3210 return;
3211 }
3212
3213 FINGERPRINT_STATES_MAPPING.forEach(function(icon) {
3214 pod.fingerprintIconElement.classList.toggle(
3215 icon.class, state == icon.state);
3216 });
3217 pod.fingerprintIconElement.hidden = false;
3218 pod.submitButton.hidden = pod.passwordElement.value.length == 0;
3219 this.updatePasswordField_(pod, state);
3220 if (state == FINGERPRINT_STATES.DEFAULT)
3221 return;
3222
3223 pod.fingerprintAuthenticated_ = true;
3224 this.setActivatedPod(pod);
3225 if (state == FINGERPRINT_STATES.FAILED) {
3226 /** @const */ var RESET_ICON_TIMEOUT_MS = 500;
3227 setTimeout(
3228 this.resetIconAndPasswordField_.bind(this, pod),
3229 RESET_ICON_TIMEOUT_MS);
3230 }
3231 },
3232
3233 /**
3234 * Reset the fingerprint icon and password field.
3235 * @param {UserPod} pod Pod to reset.
3236 */
3237 resetIconAndPasswordField_: function(pod) {
3238 if (!pod.fingerprintIconElement)
3239 return;
3240 this.setUserPodFingerprintIcon(
3241 pod.user.username, FINGERPRINT_STATES.DEFAULT);
3242 },
3243
3244 /**
3245 * Remove the fingerprint icon in the user pod.
3246 * @param {string} username Username of the selected user
3247 */
3248 removeUserPodFingerprintIcon: function(username) {
3249 var pod = this.getPodWithUsername_(username);
3250 if (pod == null) {
3251 console.error('No user pod found (when removing fingerprint icon).');
3252 return;
3253 }
3254 this.resetIconAndPasswordField_(pod);
3255 if (pod.fingerprintIconElement) {
3256 pod.fingerprintIconElement.parentNode.removeChild(
3257 pod.fingerprintIconElement);
3258 }
3259 pod.submitButton.hidden = false;
3260 },
3261
3262 /**
3263 * Updates the password field in the user pod.
3264 * @param {UserPod} pod Pod to update.
3265 * @param {number} state Fingerprint unlock state
3266 */
3267 updatePasswordField_: function(pod, state) {
3268 FINGERPRINT_STATES_MAPPING.forEach(function(item) {
3269 pod.passwordElement.classList.toggle(item.class, state == item.state);
3270 });
3271 var placeholderStr = loadTimeData.getString(
3272 pod.isPinShown() ? 'pinKeyboardPlaceholderPinPassword' :
3273 'passwordHint');
3274 if (state == FINGERPRINT_STATES.SIGNIN) {
3275 placeholderStr = loadTimeData.getString('fingerprintSigningin');
3276 } else if (state == FINGERPRINT_STATES.FAILED) {
3277 placeholderStr = loadTimeData.getString('fingerprintSigninFailed');
3278 }
3279 pod.passwordElement.placeholder = placeholderStr;
3280 },
3281
3282 /**
3283 * Sets the authentication type used to authenticate the user.
3284 * @param {string} username Username of selected user
3285 * @param {number} authType Authentication type, must be one of the
3286 * values listed in AUTH_TYPE enum.
3287 * @param {string} value The initial value to use for authentication.
3288 */
3289 setAuthType: function(username, authType, value) {
3290 var pod = this.getPodWithUsername_(username);
3291 if (pod == null) {
3292 console.error('Unable to set auth type: user pod not found.');
3293 return;
3294 }
3295 pod.setAuthType(authType, value);
3296 },
3297
3298 /**
3299 * Sets the state of touch view mode.
3300 * @param {boolean} isTouchViewEnabled true if the mode is on.
3301 */
3302 setTouchViewState: function(isTouchViewEnabled) {
3303 this.touchViewEnabled_ = isTouchViewEnabled;
3304 this.pods.forEach(function(pod, index) {
3305 pod.actionBoxAreaElement.classList.toggle('forced', isTouchViewEnabled);
3306 });
3307 },
3308
3309 /**
3310 * Updates the display name shown on a public session pod.
3311 * @param {string} userID The user ID of the public session
3312 * @param {string} displayName The new display name
3313 */
3314 setPublicSessionDisplayName: function(userID, displayName) {
3315 var pod = this.getPodWithUsername_(userID);
3316 if (pod != null)
3317 pod.setDisplayName(displayName);
3318 },
3319
3320 /**
3321 * Updates the list of locales available for a public session.
3322 * @param {string} userID The user ID of the public session
3323 * @param {!Object} locales The list of available locales
3324 * @param {string} defaultLocale The locale to select by default
3325 * @param {boolean} multipleRecommendedLocales Whether |locales| contains
3326 * two or more recommended locales
3327 */
3328 setPublicSessionLocales: function(userID,
3329 locales,
3330 defaultLocale,
3331 multipleRecommendedLocales) {
3332 var pod = this.getPodWithUsername_(userID);
3333 if (pod != null) {
3334 pod.populateLanguageSelect(locales,
3335 defaultLocale,
3336 multipleRecommendedLocales);
3337 }
3338 },
3339
3340 /**
3341 * Updates the list of available keyboard layouts for a public session pod.
3342 * @param {string} userID The user ID of the public session
3343 * @param {string} locale The locale to which this list of keyboard layouts
3344 * applies
3345 * @param {!Object} list List of available keyboard layouts
3346 */
3347 setPublicSessionKeyboardLayouts: function(userID, locale, list) {
3348 var pod = this.getPodWithUsername_(userID);
3349 if (pod != null)
3350 pod.populateKeyboardSelect(locale, list);
3351 },
3352
3353 /**
3354 * Called when window was resized.
3355 */
3356 onWindowResize: function() {
3357 var layout = this.calculateLayout_();
3358 if (layout.columns != this.columns || layout.rows != this.rows)
3359 this.placePods_();
3360
3361 // Wrap this in a set timeout so the function is called after the pod is
3362 // finished transitioning so that we work with the final pod dimensions.
3363 // If there is no focused pod that may be transitioning when this function
3364 // is called, we can call scrollFocusedPodIntoView() right away.
3365 var timeOut = 0;
3366 if (this.focusedPod_) {
3367 var style = getComputedStyle(this.focusedPod_);
3368 timeOut = parseFloat(style.transitionDuration) * 1000;
3369 }
3370
3371 setTimeout(function() {
3372 this.scrollFocusedPodIntoView();
3373 }.bind(this), timeOut);
3374 },
3375
3376 /**
3377 * Returns width of podrow having |columns| number of columns.
3378 * @private
3379 */
3380 columnsToWidth_: function(columns) {
3381 var isDesktopUserManager = Oobe.getInstance().displayType ==
3382 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
3383 var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
3384 MARGIN_BY_COLUMNS[columns];
3385 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
3386 POD_ROW_PADDING;
3387 return 2 * rowPadding + columns * this.userPodWidth_ +
3388 (columns - 1) * margin;
3389 },
3390
3391 /**
3392 * Returns height of podrow having |rows| number of rows.
3393 * @private
3394 */
3395 rowsToHeight_: function(rows) {
3396 var isDesktopUserManager = Oobe.getInstance().displayType ==
3397 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
3398 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
3399 POD_ROW_PADDING;
3400 return 2 * rowPadding + rows * this.userPodHeight_;
3401 },
3402
3403 /**
3404 * Calculates number of columns and rows that podrow should have in order to
3405 * hold as much its pods as possible for current screen size. Also it tries
3406 * to choose layout that looks good.
3407 * @return {{columns: number, rows: number}}
3408 */
3409 calculateLayout_: function() {
3410 var preferredColumns = this.pods.length < COLUMNS.length ?
3411 COLUMNS[this.pods.length] : COLUMNS[COLUMNS.length - 1];
3412 var maxWidth = Oobe.getInstance().clientAreaSize.width;
3413 var columns = preferredColumns;
3414 while (maxWidth < this.columnsToWidth_(columns) && columns > 1)
3415 --columns;
3416 var rows = Math.floor((this.pods.length - 1) / columns) + 1;
3417 if (getComputedStyle(
3418 $('signin-banner'), null).getPropertyValue('display') != 'none') {
3419 rows = Math.min(rows, MAX_NUMBER_OF_ROWS_UNDER_SIGNIN_BANNER);
3420 }
3421 if (!Oobe.getInstance().newDesktopUserManager) {
3422 var maxHeigth = Oobe.getInstance().clientAreaSize.height;
3423 while (maxHeigth < this.rowsToHeight_(rows) && rows > 1)
3424 --rows;
3425 }
3426 // One more iteration if it's not enough cells to place all pods.
3427 while (maxWidth >= this.columnsToWidth_(columns + 1) &&
3428 columns * rows < this.pods.length &&
3429 columns < MAX_NUMBER_OF_COLUMNS) {
3430 ++columns;
3431 }
3432 return {columns: columns, rows: rows};
3433 },
3434
3435 /**
3436 * Places pods onto their positions onto pod grid.
3437 * @private
3438 */
3439 placePods_: function() {
3440 var isDesktopUserManager = Oobe.getInstance().displayType ==
3441 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
3442 if (isDesktopUserManager && !Oobe.getInstance().userPodsPageVisible)
3443 return;
3444
3445 var layout = this.calculateLayout_();
3446 var columns = this.columns = layout.columns;
3447 var rows = this.rows = layout.rows;
3448 var maxPodsNumber = columns * rows;
3449 var margin = isDesktopUserManager ? DESKTOP_MARGIN_BY_COLUMNS[columns] :
3450 MARGIN_BY_COLUMNS[columns];
3451 this.parentNode.setPreferredSize(
3452 this.columnsToWidth_(columns), this.rowsToHeight_(rows));
3453 var height = this.userPodHeight_;
3454 var width = this.userPodWidth_;
3455 var pinPodLocation = { column: columns + 1, row: rows + 1 };
3456 if (this.focusedPod_ && this.focusedPod_.isPinShown())
3457 pinPodLocation = this.findPodLocation_(this.focusedPod_, columns, rows);
3458
3459 this.pods.forEach(function(pod, index) {
3460 if (index >= maxPodsNumber) {
3461 pod.hidden = true;
3462 return;
3463 }
3464 pod.hidden = false;
3465 if (pod.offsetHeight != height &&
3466 pod.offsetHeight != CROS_PIN_POD_HEIGHT) {
3467 console.error('Pod offsetHeight (' + pod.offsetHeight +
3468 ') and POD_HEIGHT (' + height + ') are not equal.');
3469 }
3470 if (pod.offsetWidth != width) {
3471 console.error('Pod offsetWidth (' + pod.offsetWidth +
3472 ') and POD_WIDTH (' + width + ') are not equal.');
3473 }
3474 var column = index % columns;
3475 var row = Math.floor(index / columns);
3476
3477 var rowPadding = isDesktopUserManager ? DESKTOP_ROW_PADDING :
3478 POD_ROW_PADDING;
3479 pod.left = rowPadding + column * (width + margin);
3480
3481 // On desktop, we want the rows to always be equally spaced.
3482 pod.top = isDesktopUserManager ? row * (height + rowPadding) :
3483 row * height + rowPadding;
3484 });
3485 Oobe.getInstance().updateScreenSize(this.parentNode);
3486 },
3487
3488 /**
3489 * Number of columns.
3490 * @type {?number}
3491 */
3492 set columns(columns) {
3493 // Cannot use 'columns' here.
3494 this.setAttribute('ncolumns', columns);
3495 },
3496 get columns() {
3497 return parseInt(this.getAttribute('ncolumns'));
3498 },
3499
3500 /**
3501 * Number of rows.
3502 * @type {?number}
3503 */
3504 set rows(rows) {
3505 // Cannot use 'rows' here.
3506 this.setAttribute('nrows', rows);
3507 },
3508 get rows() {
3509 return parseInt(this.getAttribute('nrows'));
3510 },
3511
3512 /**
3513 * Whether the pod is currently focused.
3514 * @param {UserPod} pod Pod to check for focus.
3515 * @return {boolean} Pod focus status.
3516 */
3517 isFocused: function(pod) {
3518 return this.focusedPod_ == pod;
3519 },
3520
3521 /**
3522 * Focuses a given user pod or clear focus when given null.
3523 * @param {UserPod=} podToFocus User pod to focus (undefined clears focus).
3524 * @param {boolean=} opt_force If true, forces focus update even when
3525 * podToFocus is already focused.
3526 * @param {boolean=} opt_skipInputFocus If true, don't focus on the input
3527 * box of user pod.
3528 */
3529 focusPod: function(podToFocus, opt_force, opt_skipInputFocus) {
3530 if (this.isFocused(podToFocus) && !opt_force) {
3531 // Calling focusPod w/o podToFocus means reset.
3532 if (!podToFocus)
3533 Oobe.clearErrors();
3534 return;
3535 }
3536
3537 // Make sure there's only one focusPod operation happening at a time.
3538 if (this.insideFocusPod_) {
3539 return;
3540 }
3541 this.insideFocusPod_ = true;
3542
3543 for (var i = 0, pod; pod = this.pods[i]; ++i) {
3544 if (!this.alwaysFocusSinglePod) {
3545 pod.isActionBoxMenuActive = false;
3546 }
3547 if (pod != podToFocus) {
3548 pod.isActionBoxMenuHovered = false;
3549 pod.classList.remove('focused');
3550 pod.setPinVisibility(false);
3551 this.setUserPodFingerprintIcon(
3552 pod.user.username, FINGERPRINT_STATES.HIDDEN);
3553 // On Desktop, the faded style is not set correctly, so we should
3554 // manually fade out non-focused pods if there is a focused pod.
3555 if (pod.user.isDesktopUser && podToFocus)
3556 pod.classList.add('faded');
3557 else
3558 pod.classList.remove('faded');
3559 pod.reset(false);
3560 }
3561 }
3562
3563 // Clear any error messages for previous pod.
3564 if (!this.isFocused(podToFocus))
3565 Oobe.clearErrors();
3566
3567 this.focusedPod_ = podToFocus;
3568 if (podToFocus) {
3569 // Only show the keyboard if it is fully loaded.
3570 if (podToFocus.isPinReady())
3571 podToFocus.setPinVisibility(true);
3572 podToFocus.classList.remove('faded');
3573 podToFocus.classList.add('focused');
3574 if (!podToFocus.multiProfilesPolicyApplied) {
3575 podToFocus.classList.toggle('signing-in', false);
3576 if (!opt_skipInputFocus)
3577 podToFocus.focusInput();
3578 } else {
3579 podToFocus.userTypeBubbleElement.classList.add('bubble-shown');
3580 // Note it is not necessary to skip this focus request when
3581 // |opt_skipInputFocus| is true. When |multiProfilesPolicyApplied|
3582 // is false, it doesn't focus on the password input box by default.
3583 podToFocus.focus();
3584 }
3585
3586 // focusPod() automatically loads wallpaper
3587 if (!podToFocus.user.isApp)
3588 chrome.send('focusPod', [podToFocus.user.username]);
3589 this.firstShown_ = false;
3590 this.lastFocusedPod_ = podToFocus;
3591 this.scrollFocusedPodIntoView();
3592 this.setUserPodFingerprintIcon(
3593 podToFocus.user.username, FINGERPRINT_STATES.DEFAULT);
3594 } else {
3595 chrome.send('noPodFocused');
3596 }
3597 this.insideFocusPod_ = false;
3598 },
3599
3600 /**
3601 * Resets wallpaper to the last active user's wallpaper, if any.
3602 */
3603 loadLastWallpaper: function() {
3604 if (this.lastFocusedPod_ && !this.lastFocusedPod_.user.isApp)
3605 chrome.send('loadWallpaper', [this.lastFocusedPod_.user.username]);
3606 },
3607
3608 /**
3609 * Returns the currently activated pod.
3610 * @type {UserPod}
3611 */
3612 get activatedPod() {
3613 return this.activatedPod_;
3614 },
3615
3616 /**
3617 * Sets currently activated pod.
3618 * @param {UserPod} pod Pod to check for focus.
3619 * @param {Event} e Event object.
3620 */
3621 setActivatedPod: function(pod, e) {
3622 if (this.disabled) {
3623 console.error('Cannot activate pod while sign-in UI is disabled.');
3624 return;
3625 }
3626 if (pod && pod.activate(e))
3627 this.activatedPod_ = pod;
3628 },
3629
3630 /**
3631 * The pod of the signed-in user, if any; null otherwise.
3632 * @type {?UserPod}
3633 */
3634 get lockedPod() {
3635 for (var i = 0, pod; pod = this.pods[i]; ++i) {
3636 if (pod.user.signedIn)
3637 return pod;
3638 }
3639 return null;
3640 },
3641
3642 /**
3643 * The pod that is preselected on user pod row show.
3644 * @type {?UserPod}
3645 */
3646 get preselectedPod() {
3647 var isDesktopUserManager = Oobe.getInstance().displayType ==
3648 DISPLAY_TYPE.DESKTOP_USER_MANAGER;
3649 if (isDesktopUserManager) {
3650 // On desktop, don't pre-select a pod if it's the only one.
3651 if (this.pods.length == 1)
3652 return null;
3653
3654 // The desktop User Manager can send an URI encoded profile path in the
3655 // url hash, that indicates a pod that should be initially focused.
3656 var focusedProfilePath =
3657 decodeURIComponent(window.location.hash.substr(1));
3658 for (var i = 0, pod; pod = this.pods[i]; ++i) {
3659 if (focusedProfilePath === pod.user.profilePath)
3660 return pod;
3661 }
3662 return null;
3663 }
3664
3665 var lockedPod = this.lockedPod;
3666 if (lockedPod)
3667 return lockedPod;
3668 for (i = 0; pod = this.pods[i]; ++i) {
3669 if (!pod.multiProfilesPolicyApplied)
3670 return pod;
3671 }
3672 return this.pods[0];
3673 },
3674
3675 /**
3676 * Resets input UI.
3677 * @param {boolean} takeFocus True to take focus.
3678 */
3679 reset: function(takeFocus) {
3680 this.disabled = false;
3681 if (this.activatedPod_)
3682 this.activatedPod_.reset(takeFocus);
3683 },
3684
3685 /**
3686 * Restores input focus to current selected pod, if there is any.
3687 */
3688 refocusCurrentPod: function() {
3689 if (this.focusedPod_ && !this.focusedPod_.multiProfilesPolicyApplied) {
3690 this.focusedPod_.focusInput();
3691 }
3692 },
3693
3694 /**
3695 * Clears focused pod password field.
3696 */
3697 clearFocusedPod: function() {
3698 if (!this.disabled && this.focusedPod_)
3699 this.focusedPod_.reset(true);
3700 },
3701
3702 /**
3703 * Shows signin UI.
3704 * @param {string} email Email for signin UI.
3705 */
3706 showSigninUI: function(email) {
3707 // Clear any error messages that might still be around.
3708 Oobe.clearErrors();
3709 this.disabled = true;
3710 this.lastFocusedPod_ = this.getPodWithUsername_(email);
3711 Oobe.showSigninUI(email);
3712 },
3713
3714 /**
3715 * Updates current image of a user.
3716 * @param {string} username User for which to update the image.
3717 */
3718 updateUserImage: function(username) {
3719 var pod = this.getPodWithUsername_(username);
3720 if (pod)
3721 pod.updateUserImage();
3722 },
3723
3724 /**
3725 * Handler of click event.
3726 * @param {Event} e Click Event object.
3727 * @private
3728 */
3729 handleClick_: function(e) {
3730 if (this.disabled)
3731 return;
3732
3733 // Clear all menus if the click is outside pod menu and its
3734 // button area.
3735 if (!findAncestorByClass(e.target, 'action-box-menu') &&
3736 !findAncestorByClass(e.target, 'action-box-area')) {
3737 for (var i = 0, pod; pod = this.pods[i]; ++i)
3738 pod.isActionBoxMenuActive = false;
3739 }
3740
3741 // Clears focus if not clicked on a pod and if there's more than one pod.
3742 var pod = findAncestorByClass(e.target, 'pod');
3743 if ((!pod || pod.parentNode != this) && !this.alwaysFocusSinglePod) {
3744 this.focusPod();
3745 }
3746
3747 if (pod)
3748 pod.isActionBoxMenuHovered = true;
3749
3750 // Return focus back to single pod.
3751 if (this.alwaysFocusSinglePod && !pod) {
3752 if ($('login-header-bar').contains(e.target))
3753 return;
3754 this.focusPod(this.focusedPod_, true /* force */);
3755 this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
3756 this.focusedPod_.isActionBoxMenuHovered = false;
3757 }
3758 },
3759
3760 /**
3761 * Handler of mouse move event.
3762 * @param {Event} e Click Event object.
3763 * @private
3764 */
3765 handleMouseMove_: function(e) {
3766 if (this.disabled)
3767 return;
3768 if (e.movementX == 0 && e.movementY == 0)
3769 return;
3770
3771 // Defocus (thus hide) action box, if it is focused on a user pod
3772 // and the pointer is not hovering over it.
3773 var pod = findAncestorByClass(e.target, 'pod');
3774 if (document.activeElement &&
3775 document.activeElement.parentNode != pod &&
3776 document.activeElement.classList.contains('action-box-area')) {
3777 document.activeElement.parentNode.focus();
3778 }
3779
3780 if (pod)
3781 pod.isActionBoxMenuHovered = true;
3782
3783 // Hide action boxes on other user pods.
3784 for (var i = 0, p; p = this.pods[i]; ++i)
3785 if (p != pod && !p.isActionBoxMenuActive)
3786 p.isActionBoxMenuHovered = false;
3787 },
3788
3789 /**
3790 * Handles focus event.
3791 * @param {Event} e Focus Event object.
3792 * @private
3793 */
3794 handleFocus_: function(e) {
3795 if (this.disabled)
3796 return;
3797 if (e.target.parentNode == this) {
3798 // Focus on a pod
3799 if (e.target.classList.contains('focused')) {
3800 if (!e.target.multiProfilesPolicyApplied)
3801 e.target.focusInput();
3802 else
3803 e.target.userTypeBubbleElement.classList.add('bubble-shown');
3804 } else
3805 this.focusPod(e.target);
3806 return;
3807 }
3808
3809 var pod = findAncestorByClass(e.target, 'pod');
3810 if (pod && pod.parentNode == this) {
3811 // Focus on a control of a pod but not on the action area button.
3812 if (!pod.classList.contains('focused')) {
3813 if (e.target.classList.contains('action-box-area') ||
3814 e.target.classList.contains('remove-warning-button')) {
3815 // focusPod usually moves focus on the password input box which
3816 // triggers virtual keyboard to show up. But the focus may move to a
3817 // non text input element shortly by e.target.focus. Hence, a
3818 // virtual keyboard flicking might be observed. We need to manually
3819 // prevent focus on password input box to avoid virtual keyboard
3820 // flicking in this case. See crbug.com/396016 for details.
3821 this.focusPod(pod, false, true /* opt_skipInputFocus */);
3822 } else {
3823 this.focusPod(pod);
3824 }
3825 pod.userTypeBubbleElement.classList.remove('bubble-shown');
3826 e.target.focus();
3827 }
3828 return;
3829 }
3830
3831 // Clears pod focus when we reach here. It means new focus is neither
3832 // on a pod nor on a button/input for a pod.
3833 // Do not "defocus" user pod when it is a single pod.
3834 // That means that 'focused' class will not be removed and
3835 // input field/button will always be visible.
3836 if (!this.alwaysFocusSinglePod)
3837 this.focusPod();
3838 else {
3839 // Hide user-type-bubble in case this is one pod and we lost focus of
3840 // it.
3841 this.focusedPod_.userTypeBubbleElement.classList.remove('bubble-shown');
3842 }
3843 },
3844
3845 /**
3846 * Handler of keydown event.
3847 * @param {Event} e KeyDown Event object.
3848 */
3849 handleKeyDown: function(e) {
3850 if (this.disabled)
3851 return;
3852 var editing = e.target.tagName == 'INPUT' && e.target.value;
3853 switch (e.key) {
3854 case 'ArrowLeft':
3855 if (!editing) {
3856 if (this.focusedPod_ && this.focusedPod_.previousElementSibling)
3857 this.focusPod(this.focusedPod_.previousElementSibling);
3858 else
3859 this.focusPod(this.lastElementChild);
3860
3861 e.stopPropagation();
3862 }
3863 break;
3864 case 'ArrowRight':
3865 if (!editing) {
3866 if (this.focusedPod_ && this.focusedPod_.nextElementSibling)
3867 this.focusPod(this.focusedPod_.nextElementSibling);
3868 else
3869 this.focusPod(this.firstElementChild);
3870
3871 e.stopPropagation();
3872 }
3873 break;
3874 case 'Enter':
3875 if (this.focusedPod_) {
3876 var targetTag = e.target.tagName;
3877 if (e.target == this.focusedPod_.passwordElement ||
3878 (this.focusedPod_.pinKeyboard &&
3879 e.target == this.focusedPod_.pinKeyboard.inputElement) ||
3880 (targetTag != 'INPUT' &&
3881 targetTag != 'BUTTON' &&
3882 targetTag != 'A')) {
3883 this.setActivatedPod(this.focusedPod_, e);
3884 e.stopPropagation();
3885 }
3886 }
3887 break;
3888 case 'Escape':
3889 if (!this.alwaysFocusSinglePod)
3890 this.focusPod();
3891 break;
3892 }
3893 },
3894
3895 /**
3896 * Called right after the pod row is shown.
3897 */
3898 handleAfterShow: function() {
3899 var focusedPod = this.focusedPod_;
3900
3901 // Without timeout changes in pods positions will be animated even though
3902 // it happened when 'flying-pods' class was disabled.
3903 setTimeout(function() {
3904 Oobe.getInstance().toggleClass('flying-pods', true);
3905 if (focusedPod)
3906 ensureTransitionEndEvent(focusedPod);
3907 }, 0);
3908
3909 // Force input focus for user pod on show and once transition ends.
3910 if (focusedPod) {
3911 var screen = this.parentNode;
3912 var self = this;
3913 focusedPod.addEventListener('transitionend', function f(e) {
3914 focusedPod.removeEventListener('transitionend', f);
3915 focusedPod.reset(true);
3916 // Notify screen that it is ready.
3917 screen.onShow();
3918 });
3919 }
3920 },
3921
3922 /**
3923 * Called right before the pod row is shown.
3924 */
3925 handleBeforeShow: function() {
3926 Oobe.getInstance().toggleClass('flying-pods', false);
3927 for (var event in this.listeners_) {
3928 this.ownerDocument.addEventListener(
3929 event, this.listeners_[event][0], this.listeners_[event][1]);
3930 }
3931 $('login-header-bar').buttonsTabIndex = UserPodTabOrder.HEADER_BAR;
3932
3933 if (this.podPlacementPostponed_) {
3934 this.podPlacementPostponed_ = false;
3935 this.placePods_();
3936 this.maybePreselectPod();
3937 }
3938 },
3939
3940 /**
3941 * Called when the element is hidden.
3942 */
3943 handleHide: function() {
3944 for (var event in this.listeners_) {
3945 this.ownerDocument.removeEventListener(
3946 event, this.listeners_[event][0], this.listeners_[event][1]);
3947 }
3948 $('login-header-bar').buttonsTabIndex = 0;
3949 },
3950
3951 /**
3952 * Called when a pod's user image finishes loading.
3953 */
3954 handlePodImageLoad: function(pod) {
3955 var index = this.podsWithPendingImages_.indexOf(pod);
3956 if (index == -1) {
3957 return;
3958 }
3959
3960 this.podsWithPendingImages_.splice(index, 1);
3961 if (this.podsWithPendingImages_.length == 0) {
3962 this.classList.remove('images-loading');
3963 }
3964 },
3965
3966 /**
3967 * Preselects pod, if needed.
3968 */
3969 maybePreselectPod: function() {
3970 var pod = this.preselectedPod;
3971 this.focusPod(pod);
3972
3973 // Hide user-type-bubble in case all user pods are disabled and we focus
3974 // first pod.
3975 if (pod && pod.multiProfilesPolicyApplied) {
3976 pod.userTypeBubbleElement.classList.remove('bubble-shown');
3977 }
3978 }
3979 };
3980
3981 return {
3982 PodRow: PodRow
3983 };
3984 });
OLDNEW
« no previous file with comments | « ui/login/account_picker/md_user_pod_row.css ('k') | ui/login/account_picker/md_user_pod_template.css » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698