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