• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/**
6 * @fileoverview Oobe user image screen implementation.
7 */
8
9cr.define('login', function() {
10  var UserImagesGrid = options.UserImagesGrid;
11  var ButtonImages = UserImagesGrid.ButtonImages;
12
13  /**
14   * Array of button URLs used on this page.
15   * @type {Array.<string>}
16   * @const
17   */
18  var ButtonImageUrls = [
19    ButtonImages.TAKE_PHOTO
20  ];
21
22  /**
23   * Whether the web camera item should be preselected, if available.
24   * @type {boolean}
25   * @const
26   */
27  var PRESELECT_CAMERA = false;
28
29  /**
30   * Creates a new OOBE screen div.
31   * @constructor
32   * @extends {HTMLDivElement}
33   */
34  var UserImageScreen = cr.ui.define(login.Screen);
35
36  /**
37   * Registers with Oobe.
38   * @param {boolean} lazyInit If true, screen is decorated on first show.
39   */
40  UserImageScreen.register = function(lazyInit) {
41    var screen = $('user-image');
42    if (lazyInit) {
43      screen.__proto__ = UserImageScreen.prototype;
44      screen.deferredDecorate = function() {
45        UserImageScreen.decorate(screen);
46      };
47    } else {
48      UserImageScreen.decorate(screen);
49    }
50    Oobe.getInstance().registerScreen(screen);
51  };
52
53  UserImageScreen.prototype = {
54    __proto__: login.Screen.prototype,
55
56    /**
57     * Currently selected user image index (take photo button is with zero
58     * index).
59     * @type {number}
60     */
61    selectedUserImage_: -1,
62
63    /**
64     * Indicates if profile picture should be displayed on current screen.
65     */
66    profilePictureEnabled_: false,
67
68    /**
69     * URL for profile picture.
70     */
71    profileImageUrl_: null,
72
73    /** @override */
74    decorate: function(element) {
75      var imageGrid = $('user-image-grid');
76      UserImagesGrid.decorate(imageGrid);
77
78      // Preview image will track the selected item's URL.
79      var previewElement = $('user-image-preview');
80      previewElement.oncontextmenu = function(e) { e.preventDefault(); };
81
82      imageGrid.previewElement = previewElement;
83      imageGrid.selectionType = 'default';
84      imageGrid.flipPhotoElement = $('flip-photo');
85
86      imageGrid.addEventListener('select',
87                                 this.handleSelect_.bind(this));
88      imageGrid.addEventListener('activate',
89                                 this.handleImageActivated_.bind(this));
90      imageGrid.addEventListener('phototaken',
91                                 this.handlePhotoTaken_.bind(this));
92      imageGrid.addEventListener('photoupdated',
93                                 this.handlePhotoUpdated_.bind(this));
94
95      // Set the title for camera item in the grid.
96      imageGrid.setCameraTitles(
97          loadTimeData.getString('takePhoto'),
98          loadTimeData.getString('photoFromCamera'));
99
100      this.setProfilePictureEnabled_(true);
101
102      this.profileImageLoading = true;
103
104      $('take-photo').addEventListener(
105          'click', this.handleTakePhoto_.bind(this));
106      $('discard-photo').addEventListener(
107          'click', this.handleDiscardPhoto_.bind(this));
108
109      // Toggle 'animation' class for the duration of WebKit transition.
110      $('flip-photo').addEventListener(
111          'click', function(e) {
112            previewElement.classList.add('animation');
113            imageGrid.flipPhoto = !imageGrid.flipPhoto;
114          });
115      $('user-image-stream-crop').addEventListener(
116          'webkitTransitionEnd', function(e) {
117            previewElement.classList.remove('animation');
118          });
119      $('user-image-preview-img').addEventListener(
120          'webkitTransitionEnd', function(e) {
121            previewElement.classList.remove('animation');
122          });
123
124      this.updateLocalizedContent();
125
126      chrome.send('getImages');
127    },
128
129    /**
130     * Header text of the screen.
131     * @type {string}
132     */
133    get header() {
134      return loadTimeData.getString('userImageScreenTitle');
135    },
136
137    /**
138     * Buttons in oobe wizard's button strip.
139     * @type {array} Array of Buttons.
140     */
141    get buttons() {
142      var okButton = this.ownerDocument.createElement('button');
143      okButton.id = 'ok-button';
144      okButton.textContent = loadTimeData.getString('okButtonText');
145      okButton.addEventListener('click', this.acceptImage_.bind(this));
146      return [okButton];
147    },
148
149    /**
150     * The caption to use for the Profile image preview.
151     * @type {string}
152     */
153    get profileImageCaption() {
154      return this.profileImageCaption_;
155    },
156    set profileImageCaption(value) {
157      this.profileImageCaption_ = value;
158      this.updateCaption_();
159    },
160
161    /**
162     * True if the Profile image is being loaded.
163     * @type {boolean}
164     */
165    get profileImageLoading() {
166      return this.profileImageLoading_;
167    },
168    set profileImageLoading(value) {
169      this.profileImageLoading_ = value;
170      $('user-image-screen-main').classList[
171          value ? 'add' : 'remove']('profile-image-loading');
172      this.updateProfileImageCaption_();
173    },
174
175    /**
176     * Handles image activation (by pressing Enter).
177     * @private
178     */
179    handleImageActivated_: function() {
180      switch ($('user-image-grid').selectedItemUrl) {
181        case ButtonImages.TAKE_PHOTO:
182          this.handleTakePhoto_();
183          break;
184        default:
185          this.acceptImage_();
186          break;
187      }
188    },
189
190    /**
191     * Handles selection change.
192     * @param {Event} e Selection change event.
193     * @private
194     */
195    handleSelect_: function(e) {
196      var imageGrid = $('user-image-grid');
197      $('ok-button').disabled = false;
198
199      // Camera selection
200      if (imageGrid.selectionType == 'camera') {
201        $('flip-photo').tabIndex = 0;
202        // No current image selected.
203        if (imageGrid.cameraLive) {
204          imageGrid.previewElement.classList.remove('phototaken');
205          $('ok-button').disabled = true;
206        } else {
207          imageGrid.previewElement.classList.add('phototaken');
208          this.notifyImageSelected_();
209        }
210      } else {
211        imageGrid.previewElement.classList.remove('phototaken');
212        $('flip-photo').tabIndex = -1;
213        this.notifyImageSelected_();
214      }
215      // Start/stop camera on (de)selection.
216      if (!imageGrid.inProgramSelection &&
217          imageGrid.selectionType != e.oldSelectionType) {
218        if (imageGrid.selectionType == 'camera') {
219          // Programmatic selection of camera item is done in
220          // startCamera callback where streaming is started by itself.
221          imageGrid.startCamera(
222              function() {
223                // Start capture if camera is still the selected item.
224                $('user-image-preview-img').classList.toggle(
225                    'animated-transform', true);
226                return imageGrid.selectedItem == imageGrid.cameraImage;
227              });
228        } else {
229          $('user-image-preview-img').classList.toggle('animated-transform',
230                                                       false);
231          imageGrid.stopCamera();
232        }
233      }
234      this.updateCaption_();
235      // Update image attribution text.
236      var image = imageGrid.selectedItem;
237      $('user-image-author-name').textContent = image.author;
238      $('user-image-author-website').textContent = image.website;
239      $('user-image-author-website').href = image.website;
240      $('user-image-attribution').style.visibility =
241          (image.author || image.website) ? 'visible' : 'hidden';
242    },
243
244    /**
245     * Handle photo capture from the live camera stream.
246     */
247    handleTakePhoto_: function(e) {
248      $('user-image-grid').takePhoto();
249    },
250
251    /**
252     * Handle photo captured event.
253     * @param {Event} e Event with 'dataURL' property containing a data URL.
254     */
255    handlePhotoTaken_: function(e) {
256      chrome.send('photoTaken', [e.dataURL]);
257      this.announceAccessibleMessage_(
258          loadTimeData.getString('photoCaptureAccessibleText'));
259    },
260
261    /**
262     * Handle photo updated event.
263     * @param {Event} e Event with 'dataURL' property containing a data URL.
264     */
265    handlePhotoUpdated_: function(e) {
266      chrome.send('photoTaken', [e.dataURL]);
267    },
268
269    /**
270     * Handle discarding the captured photo.
271     */
272    handleDiscardPhoto_: function(e) {
273      var imageGrid = $('user-image-grid');
274      imageGrid.discardPhoto();
275      this.announceAccessibleMessage_(
276          loadTimeData.getString('photoDiscardAccessibleText'));
277    },
278
279    /**
280     * Add an accessible message to the page that will be announced to
281     * users who have spoken feedback on, but will be invisible to all
282     * other users. It's removed right away so it doesn't clutter the DOM.
283     */
284    announceAccessibleMessage_: function(msg) {
285      var element = document.createElement('div');
286      element.setAttribute('aria-live', 'polite');
287      element.style.position = 'relative';
288      element.style.left = '-9999px';
289      element.style.height = '0px';
290      element.innerText = msg;
291      document.body.appendChild(element);
292      window.setTimeout(function() {
293        document.body.removeChild(element);
294      }, 0);
295    },
296
297    /**
298     * Event handler that is invoked just before the screen is shown.
299     * @param {object} data Screen init payload.
300     */
301    onBeforeShow: function(data) {
302      Oobe.getInstance().headerHidden = true;
303      var imageGrid = $('user-image-grid');
304      imageGrid.updateAndFocus();
305      chrome.send('onUserImageScreenShown');
306    },
307
308    /**
309     * Event handler that is invoked just before the screen is hidden.
310     */
311    onBeforeHide: function() {
312      $('user-image-grid').stopCamera();
313    },
314
315    /**
316     * Accepts currently selected image, if possible.
317     * @private
318     */
319    acceptImage_: function() {
320      var okButton = $('ok-button');
321      if (!okButton.disabled) {
322        // This ensures that #ok-button won't be re-enabled again.
323        $('user-image-grid').disabled = true;
324        okButton.disabled = true;
325        chrome.send('onUserImageAccepted');
326      }
327    },
328
329    /**
330     * Updates user profile image.
331     * @param {?string} imageUrl Image encoded as data URL. If null, user has
332     *     the default profile image, which we don't want to show.
333     * @private
334     */
335    setProfileImage_: function(imageUrl) {
336      this.profileImageLoading = false;
337      this.profileImageUrl_ = imageUrl;
338      if (imageUrl !== null) {
339        this.profileImage_ =
340            $('user-image-grid').updateItem(this.profileImage_, imageUrl);
341      }
342    },
343
344    /**
345     * @param {boolean} present Whether camera is detected.
346     */
347    setCameraPresent_: function(present) {
348      $('user-image-grid').cameraPresent = present;
349    },
350
351    /**
352     * Controls the profile image as one of image options.
353     * @param {enabled} Whether profile image option should be displayed.
354     * @private
355     */
356    setProfilePictureEnabled_: function(enabled) {
357      if (this.profilePictureEnabled_ == enabled)
358        return;
359      this.profilePictureEnabled_ = enabled;
360      var imageGrid = $('user-image-grid');
361      if (enabled) {
362        var url = ButtonImages.PROFILE_PICTURE;
363        if (!this.profileImageLoading && this.profileImageUrl_ !== null) {
364          url = this.profileImageUrl_;
365        }
366        // Profile image data (if present).
367        this.profileImage_ = imageGrid.addItem(
368            url,                                    // Image URL.
369            loadTimeData.getString('profilePhoto'), // Title.
370            undefined,                              // Click handler.
371            0,                                      // Position.
372            this.profileImageLoading ? function(el) {
373              // Custom decorator for Profile image element.
374              var spinner = el.ownerDocument.createElement('div');
375              spinner.className = 'spinner';
376              var spinnerBg = el.ownerDocument.createElement('div');
377              spinnerBg.className = 'spinner-bg';
378              spinnerBg.appendChild(spinner);
379              el.appendChild(spinnerBg);
380              el.id = 'profile-image';
381            } : undefined);
382        this.profileImage_.type = 'profile';
383      } else {
384        imageGrid.removeItem(this.profileImage_);
385      }
386    },
387
388    /**
389     * Appends default images to the image grid. Should only be called once.
390     * @param {Array.<{url: string, author: string, website: string}>} images
391     *   An array of default images data, including URL, author and website.
392     * @private
393     */
394    setDefaultImages_: function(imagesData) {
395      var imageGrid = $('user-image-grid');
396      for (var i = 0, data; data = imagesData[i]; i++) {
397        var item = imageGrid.addItem(data.url, data.title);
398        item.type = 'default';
399        item.author = data.author || '';
400        item.website = data.website || '';
401      }
402      chrome.send('screenReady');
403    },
404
405    /**
406     * Selects user image with the given URL.
407     * @param {string} url URL of the image to select.
408     * @private
409     */
410    setSelectedImage_: function(url) {
411      var imageGrid = $('user-image-grid');
412      imageGrid.selectedItemUrl = url;
413      imageGrid.focus();
414    },
415
416    /**
417     * Hides curtain with spinner.
418     * @private
419     */
420    hideCurtain_: function() {
421      this.classList.remove('loading');
422      Oobe.getInstance().updateScreenSize(this);
423    },
424
425    /**
426     * Updates the image preview caption.
427     * @private
428     */
429    updateCaption_: function() {
430      $('user-image-preview-caption').textContent =
431          $('user-image-grid').selectionType == 'profile' ?
432          this.profileImageCaption : '';
433    },
434
435    /**
436     * Updates localized content of the screen that is not updated via template.
437     */
438    updateLocalizedContent: function() {
439      this.updateProfileImageCaption_();
440    },
441
442    /**
443     * Updates profile image caption.
444     * @private
445     */
446    updateProfileImageCaption_: function() {
447      this.profileImageCaption = loadTimeData.getString(
448        this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto');
449    },
450
451    /**
452     * Notifies chrome about image selection.
453     * @private
454     */
455    notifyImageSelected_: function() {
456      var imageGrid = $('user-image-grid');
457      chrome.send('selectImage',
458                  [imageGrid.selectedItemUrl,
459                   imageGrid.selectionType,
460                   !imageGrid.inProgramSelection]);
461    },
462  };
463
464  // Forward public APIs to private implementations.
465  [
466    'setDefaultImages',
467    'setCameraPresent',
468    'setProfilePictureEnabled',
469    'setProfileImage',
470    'setSelectedImage',
471    'hideCurtain'
472  ].forEach(function(name) {
473    UserImageScreen[name] = function(value) {
474      $('user-image')[name + '_'](value);
475    };
476  });
477
478  return {
479    UserImageScreen: UserImageScreen
480  };
481});
482
483