1// Copyright (c) 2013 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'use strict'; 6 7/** 8 * PreviewPanel UI class. 9 * @param {HTMLElement} element DOM Element of preview panel. 10 * @param {PreviewPanel.VisibilityType} visibilityType Initial value of the 11 * visibility type. 12 * @param {MetadataCache} metadataCache Metadata cache. 13 * @param {VolumeManagerWrapper} volumeManager Volume manager. 14 * @constructor 15 * @extends {cr.EventTarget} 16 */ 17var PreviewPanel = function(element, 18 visibilityType, 19 metadataCache, 20 volumeManager) { 21 /** 22 * The cached height of preview panel. 23 * @type {number} 24 * @private 25 */ 26 this.height_ = 0; 27 28 /** 29 * Visibility type of the preview panel. 30 * @type {PreviewPanel.VisiblityType} 31 * @private 32 */ 33 this.visibilityType_ = visibilityType; 34 35 /** 36 * Current entry to be displayed. 37 * @type {Entry} 38 * @private 39 */ 40 this.currentEntry_ = null; 41 42 /** 43 * Dom element of the preview panel. 44 * @type {HTMLElement} 45 * @private 46 */ 47 this.element_ = element; 48 49 /** 50 * @type {BreadcrumbsController} 51 */ 52 this.breadcrumbs = new BreadcrumbsController( 53 element.querySelector('#search-breadcrumbs'), 54 metadataCache, 55 volumeManager); 56 57 /** 58 * @type {PreviewPanel.Thumbnails} 59 */ 60 this.thumbnails = new PreviewPanel.Thumbnails( 61 element.querySelector('.preview-thumbnails'), 62 metadataCache, 63 volumeManager); 64 65 /** 66 * @type {HTMLElement} 67 * @private 68 */ 69 this.summaryElement_ = element.querySelector('.preview-summary'); 70 71 /** 72 * @type {PreviewPanel.CalculatingSizeLabel} 73 * @private 74 */ 75 this.calculatingSizeLabel_ = new PreviewPanel.CalculatingSizeLabel( 76 this.summaryElement_.querySelector('.calculating-size')); 77 78 /** 79 * @type {HTMLElement} 80 * @private 81 */ 82 this.previewText_ = element.querySelector('.preview-text'); 83 84 /** 85 * FileSelection to be displayed. 86 * @type {FileSelection} 87 * @private 88 */ 89 this.selection_ = {entries: [], computeBytes: function() {}}; 90 91 /** 92 * Sequence value that is incremented by every selection update and is used to 93 * check if the callback is up to date or not. 94 * @type {number} 95 * @private 96 */ 97 this.sequence_ = 0; 98 99 /** 100 * @type {VolumeManagerWrapper} 101 * @private 102 */ 103 this.volumeManager_ = volumeManager; 104 105 cr.EventTarget.call(this); 106}; 107 108/** 109 * Name of PreviewPanels's event. 110 * @enum {string} 111 * @const 112 */ 113PreviewPanel.Event = Object.freeze({ 114 // Event to be triggered at the end of visibility change. 115 VISIBILITY_CHANGE: 'visibilityChange' 116}); 117 118/** 119 * Visibility type of the preview panel. 120 */ 121PreviewPanel.VisibilityType = Object.freeze({ 122 // Preview panel always shows. 123 ALWAYS_VISIBLE: 'alwaysVisible', 124 // Preview panel shows when the selection property are set. 125 AUTO: 'auto', 126 // Preview panel does not show. 127 ALWAYS_HIDDEN: 'alwaysHidden' 128}); 129 130/** 131 * @private 132 */ 133PreviewPanel.Visibility_ = Object.freeze({ 134 VISIBLE: 'visible', 135 HIDING: 'hiding', 136 HIDDEN: 'hidden' 137}); 138 139PreviewPanel.prototype = { 140 __proto__: cr.EventTarget.prototype, 141 142 /** 143 * Setter for the current entry. 144 * @param {Entry} entry New entry. 145 */ 146 set currentEntry(entry) { 147 if (util.isSameEntry(this.currentEntry_, entry)) 148 return; 149 this.currentEntry_ = entry; 150 this.updateVisibility_(); 151 this.updatePreviewArea_(); 152 }, 153 154 /** 155 * Setter for the visibility type. 156 * @param {PreviewPanel.VisibilityType} visibilityType New value of visibility 157 * type. 158 */ 159 set visibilityType(visibilityType) { 160 this.visibilityType_ = visibilityType; 161 this.updateVisibility_(); 162 // Also update the preview area contents, because the update is suppressed 163 // while the visibility is hiding or hidden. 164 this.updatePreviewArea_(); 165 }, 166 167 get visible() { 168 return this.element_.getAttribute('visibility') == 169 PreviewPanel.Visibility_.VISIBLE; 170 }, 171 172 /** 173 * Obtains the height of preview panel. 174 * @return {number} Height of preview panel. 175 */ 176 get height() { 177 this.height_ = this.height_ || this.element_.clientHeight; 178 return this.height_; 179 } 180}; 181 182/** 183 * Initializes the element. 184 */ 185PreviewPanel.prototype.initialize = function() { 186 this.element_.addEventListener('webkitTransitionEnd', 187 this.onTransitionEnd_.bind(this)); 188 this.updateVisibility_(); 189 // Also update the preview area contents, because the update is suppressed 190 // while the visibility is hiding or hidden. 191 this.updatePreviewArea_(); 192}; 193 194/** 195 * Apply the selection and update the view of the preview panel. 196 * @param {FileSelection} selection Selection to be applied. 197 */ 198PreviewPanel.prototype.setSelection = function(selection) { 199 this.sequence_++; 200 this.selection_ = selection; 201 this.updateVisibility_(); 202 this.updatePreviewArea_(); 203}; 204 205/** 206 * Update the visibility of the preview panel. 207 * @private 208 */ 209PreviewPanel.prototype.updateVisibility_ = function() { 210 // Get the new visibility value. 211 var visibility = this.element_.getAttribute('visibility'); 212 var newVisible = null; 213 switch (this.visibilityType_) { 214 case PreviewPanel.VisibilityType.ALWAYS_VISIBLE: 215 newVisible = true; 216 break; 217 case PreviewPanel.VisibilityType.AUTO: 218 newVisible = 219 this.selection_.entries.length !== 0 || 220 (this.currentEntry_ && 221 !this.volumeManager_.getLocationInfo( 222 this.currentEntry_).isRootEntry); 223 break; 224 case PreviewPanel.VisibilityType.ALWAYS_HIDDEN: 225 newVisible = false; 226 break; 227 default: 228 console.error('Invalid visibilityType.'); 229 return; 230 } 231 232 // If the visibility has been already the new value, just return. 233 if ((visibility == PreviewPanel.Visibility_.VISIBLE && newVisible) || 234 (visibility == PreviewPanel.Visibility_.HIDDEN && !newVisible)) 235 return; 236 237 // Set the new visibility value. 238 if (newVisible) { 239 this.element_.setAttribute('visibility', PreviewPanel.Visibility_.VISIBLE); 240 cr.dispatchSimpleEvent(this, PreviewPanel.Event.VISIBILITY_CHANGE); 241 } else { 242 this.element_.setAttribute('visibility', PreviewPanel.Visibility_.HIDING); 243 } 244}; 245 246/** 247 * Update the text in the preview panel. 248 * 249 * @param {boolean} breadCrumbsVisible Whether the bread crumbs is visible or 250 * not. 251 * @private 252 */ 253PreviewPanel.prototype.updatePreviewArea_ = function(breadCrumbsVisible) { 254 // If the preview panel is hiding, does not update the current view. 255 if (!this.visible) 256 return; 257 var selection = this.selection_; 258 259 // Update thumbnails. 260 this.thumbnails.selection = selection.totalCount !== 0 ? 261 selection : {entries: [this.currentEntry_]}; 262 263 // Check if the breadcrumb list should show instead on the preview text. 264 var entry; 265 if (this.selection_.totalCount == 1) 266 entry = this.selection_.entries[0]; 267 else if (this.selection_.totalCount == 0) 268 entry = this.currentEntry_; 269 270 if (entry) { 271 this.breadcrumbs.show(entry); 272 this.calculatingSizeLabel_.hidden = true; 273 this.previewText_.textContent = ''; 274 return; 275 } 276 this.breadcrumbs.hide(); 277 278 // Obtains the preview text. 279 var text; 280 if (selection.directoryCount == 0) 281 text = strf('MANY_FILES_SELECTED', selection.fileCount); 282 else if (selection.fileCount == 0) 283 text = strf('MANY_DIRECTORIES_SELECTED', selection.directoryCount); 284 else 285 text = strf('MANY_ENTRIES_SELECTED', selection.totalCount); 286 287 // Obtains the size of files. 288 this.calculatingSizeLabel_.hidden = selection.bytesKnown; 289 if (selection.bytesKnown && selection.showBytes) 290 text += ', ' + util.bytesToString(selection.bytes); 291 292 // Set the preview text to the element. 293 this.previewText_.textContent = text; 294 295 // Request the byte calculation if needed. 296 if (!selection.bytesKnown) { 297 this.selection_.computeBytes(function(sequence) { 298 // Selection has been already updated. 299 if (this.sequence_ != sequence) 300 return; 301 this.updatePreviewArea_(); 302 }.bind(this, this.sequence_)); 303 } 304}; 305 306/** 307 * Event handler to be called at the end of hiding transition. 308 * @param {Event} event The webkitTransitionEnd event. 309 * @private 310 */ 311PreviewPanel.prototype.onTransitionEnd_ = function(event) { 312 if (event.target != this.element_ || event.propertyName != 'opacity') 313 return; 314 var visibility = this.element_.getAttribute('visibility'); 315 if (visibility != PreviewPanel.Visibility_.HIDING) 316 return; 317 this.element_.setAttribute('visibility', PreviewPanel.Visibility_.HIDDEN); 318 cr.dispatchSimpleEvent(this, PreviewPanel.Event.VISIBILITY_CHANGE); 319}; 320 321/** 322 * Animating label that is shown during the bytes of selection entries is being 323 * calculated. 324 * 325 * This label shows dots and varying the number of dots every 326 * CalculatingSizeLabel.PERIOD milliseconds. 327 * @param {HTMLElement} element DOM element of the label. 328 * @constructor 329 */ 330PreviewPanel.CalculatingSizeLabel = function(element) { 331 this.element_ = element; 332 this.count_ = 0; 333 this.intervalID_ = null; 334 Object.seal(this); 335}; 336 337/** 338 * Time period in milliseconds. 339 * @const {number} 340 */ 341PreviewPanel.CalculatingSizeLabel.PERIOD = 500; 342 343PreviewPanel.CalculatingSizeLabel.prototype = { 344 /** 345 * Set visibility of the label. 346 * When it is displayed, the text is animated. 347 * @param {boolean} hidden Whether to hide the label or not. 348 */ 349 set hidden(hidden) { 350 this.element_.hidden = hidden; 351 if (!hidden) { 352 if (this.intervalID_ != null) 353 return; 354 this.count_ = 2; 355 this.intervalID_ = 356 setInterval(this.onStep_.bind(this), 357 PreviewPanel.CalculatingSizeLabel.PERIOD); 358 this.onStep_(); 359 } else { 360 if (this.intervalID_ == null) 361 return; 362 clearInterval(this.intervalID_); 363 this.intervalID_ = null; 364 } 365 } 366}; 367 368/** 369 * Increments the counter and updates the number of dots. 370 * @private 371 */ 372PreviewPanel.CalculatingSizeLabel.prototype.onStep_ = function() { 373 var text = str('CALCULATING_SIZE'); 374 for (var i = 0; i < ~~(this.count_ / 2) % 4; i++) { 375 text += '.'; 376 } 377 this.element_.textContent = text; 378 this.count_++; 379}; 380 381/** 382 * Thumbnails on the preview panel. 383 * 384 * @param {HTMLElement} element DOM Element of thumbnail container. 385 * @param {MetadataCache} metadataCache MetadataCache. 386 * @param {VolumeManagerWrapper} volumeManager Volume manager instance. 387 * @constructor 388 */ 389PreviewPanel.Thumbnails = function(element, metadataCache, volumeManager) { 390 this.element_ = element; 391 this.metadataCache_ = metadataCache; 392 this.volumeManager_ = volumeManager; 393 this.sequence_ = 0; 394 Object.seal(this); 395}; 396 397/** 398 * Maximum number of thumbnails. 399 * @const {number} 400 */ 401PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT = 4; 402 403/** 404 * Edge length of the thumbnail square. 405 * @const {number} 406 */ 407PreviewPanel.Thumbnails.THUMBNAIL_SIZE = 35; 408 409/** 410 * Longer edge length of zoomed thumbnail rectangle. 411 * @const {number} 412 */ 413PreviewPanel.Thumbnails.ZOOMED_THUMBNAIL_SIZE = 200; 414 415PreviewPanel.Thumbnails.prototype = { 416 /** 417 * Sets entries to be displayed in the view. 418 * @param {Array.<Entry>} value Entries. 419 */ 420 set selection(value) { 421 this.sequence_++; 422 this.loadThumbnails_(value); 423 } 424}; 425 426/** 427 * Loads thumbnail images. 428 * @param {FileSelection} selection Selection containing entries that are 429 * sources of images. 430 * @private 431 */ 432PreviewPanel.Thumbnails.prototype.loadThumbnails_ = function(selection) { 433 var entries = selection.entries; 434 this.element_.classList.remove('has-zoom'); 435 this.element_.innerText = ''; 436 var clickHandler = selection.tasks && 437 selection.tasks.executeDefault.bind(selection.tasks); 438 var length = Math.min(entries.length, 439 PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT); 440 for (var i = 0; i < length; i++) { 441 // Create a box. 442 var box = this.element_.ownerDocument.createElement('div'); 443 box.style.zIndex = PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT + 1 - i; 444 445 // Load the image. 446 if (entries[i]) { 447 FileGrid.decorateThumbnailBox(box, 448 entries[i], 449 this.metadataCache_, 450 this.volumeManager_, 451 ThumbnailLoader.FillMode.FILL, 452 FileGrid.ThumbnailQuality.LOW, 453 i == 0 && length == 1 && 454 this.setZoomedImage_.bind(this)); 455 } 456 457 // Register the click handler. 458 if (clickHandler) 459 box.addEventListener('click', clickHandler); 460 461 // Append 462 this.element_.appendChild(box); 463 } 464}; 465 466/** 467 * Create the zoomed version of image and set it to the DOM element to show the 468 * zoomed image. 469 * 470 * @param {Image} image Image to be source of the zoomed image. 471 * @param {transform} transform Transformation to be applied to the image. 472 * @private 473 */ 474PreviewPanel.Thumbnails.prototype.setZoomedImage_ = function(image, transform) { 475 if (!image) 476 return; 477 var width = image.width || 0; 478 var height = image.height || 0; 479 if (width == 0 || 480 height == 0 || 481 (width < PreviewPanel.Thumbnails.THUMBNAIL_SIZE * 2 && 482 height < PreviewPanel.Thumbnails.THUMBNAIL_SIZE * 2)) 483 return; 484 485 var scale = Math.min(1, 486 PreviewPanel.Thumbnails.ZOOMED_THUMBNAIL_SIZE / 487 Math.max(width, height)); 488 var imageWidth = ~~(width * scale); 489 var imageHeight = ~~(height * scale); 490 var zoomedImage = this.element_.ownerDocument.createElement('img'); 491 492 if (scale < 0.3) { 493 // Scaling large images kills animation. Downscale it in advance. 494 // Canvas scales images with liner interpolation. Make a larger 495 // image (but small enough to not kill animation) and let IMAGE 496 // scale it smoothly. 497 var INTERMEDIATE_SCALE = 3; 498 var canvas = this.element_.ownerDocument.createElement('canvas'); 499 canvas.width = imageWidth * INTERMEDIATE_SCALE; 500 canvas.height = imageHeight * INTERMEDIATE_SCALE; 501 var ctx = canvas.getContext('2d'); 502 ctx.drawImage(image, 0, 0, canvas.width, canvas.height); 503 // Using bigger than default compression reduces image size by 504 // several times. Quality degradation compensated by greater resolution. 505 zoomedImage.src = canvas.toDataURL('image/jpeg', 0.6); 506 } else { 507 zoomedImage.src = image.src; 508 } 509 510 var boxWidth = Math.max(PreviewPanel.Thumbnails.THUMBNAIL_SIZE, imageWidth); 511 var boxHeight = Math.max(PreviewPanel.Thumbnails.THUMBNAIL_SIZE, imageHeight); 512 if (transform && transform.rotate90 % 2 == 1) { 513 var t = boxWidth; 514 boxWidth = boxHeight; 515 boxHeight = t; 516 } 517 518 util.applyTransform(zoomedImage, transform); 519 520 var zoomedBox = this.element_.ownerDocument.createElement('div'); 521 zoomedBox.className = 'popup'; 522 zoomedBox.style.width = boxWidth + 'px'; 523 zoomedBox.style.height = boxHeight + 'px'; 524 zoomedBox.appendChild(zoomedImage); 525 526 this.element_.appendChild(zoomedBox); 527 this.element_.classList.add('has-zoom'); 528 return; 529}; 530