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 'use strict'; 6 7 /** 8 * The current selection object. 9 * 10 * @param {FileManager} fileManager FileManager instance. 11 * @param {Array.<number>} indexes Selected indexes. 12 * @constructor 13 */ 14 function FileSelection(fileManager, indexes) { 15 this.fileManager_ = fileManager; 16 this.computeBytesSequence_ = 0; 17 this.indexes = indexes; 18 this.entries = []; 19 this.urls = []; 20 this.totalCount = 0; 21 this.fileCount = 0; 22 this.directoryCount = 0; 23 this.bytes = 0; 24 this.showBytes = false; 25 this.allDriveFilesPresent = false, 26 this.iconType = null; 27 this.bytesKnown = false; 28 this.mustBeHidden_ = false; 29 30 // Synchronously compute what we can. 31 for (var i = 0; i < this.indexes.length; i++) { 32 var entry = fileManager.getFileList().item(this.indexes[i]); 33 if (!entry) 34 continue; 35 36 this.entries.push(entry); 37 this.urls.push(entry.toURL()); 38 39 if (this.iconType == null) { 40 this.iconType = FileType.getIcon(entry); 41 } else if (this.iconType != 'unknown') { 42 var iconType = FileType.getIcon(entry); 43 if (this.iconType != iconType) 44 this.iconType = 'unknown'; 45 } 46 47 if (entry.isFile) { 48 this.fileCount += 1; 49 } else { 50 this.directoryCount += 1; 51 } 52 this.totalCount++; 53 } 54 55 this.tasks = new FileTasks(this.fileManager_); 56 } 57 58 /** 59 * Computes data required to get file tasks and requests the tasks. 60 * 61 * @param {function} callback The callback. 62 */ 63 FileSelection.prototype.createTasks = function(callback) { 64 if (!this.fileManager_.isOnDrive()) { 65 this.tasks.init(this.urls); 66 callback(); 67 return; 68 } 69 70 this.fileManager_.metadataCache_.get(this.urls, 'drive', function(props) { 71 var present = props.filter(function(p) { return p && p.availableOffline }); 72 this.allDriveFilesPresent = present.length == props.length; 73 74 // Collect all of the mime types and push that info into the selection. 75 this.mimeTypes = props.map(function(value) { 76 return (value && value.contentMimeType) || ''; 77 }); 78 79 this.tasks.init(this.urls, this.mimeTypes); 80 callback(); 81 }.bind(this)); 82 }; 83 84 /** 85 * Computes the total size of selected files. 86 * 87 * @param {function} callback Completion callback. Not called when cancelled, 88 * or a new call has been invoked in the meantime. 89 */ 90 FileSelection.prototype.computeBytes = function(callback) { 91 if (this.entries.length == 0) { 92 this.bytesKnown = true; 93 this.showBytes = false; 94 this.bytes = 0; 95 return; 96 } 97 98 var computeBytesSequence = ++this.computeBytesSequence_; 99 var pendingMetadataCount = 0; 100 101 var maybeDone = function() { 102 if (pendingMetadataCount == 0) { 103 this.bytesKnown = true; 104 callback(); 105 } 106 }.bind(this); 107 108 var onProps = function(properties) { 109 // Ignore if the call got cancelled, or there is another new one fired. 110 if (computeBytesSequence != this.computeBytesSequence_) 111 return; 112 113 // It may happen that the metadata is not available because a file has been 114 // deleted in the meantime. 115 if (properties) 116 this.bytes += properties.size; 117 pendingMetadataCount--; 118 maybeDone(); 119 }.bind(this); 120 121 for (var index = 0; index < this.entries.length; index++) { 122 var entry = this.entries[index]; 123 if (entry.isFile) { 124 this.showBytes |= !FileType.isHosted(entry); 125 pendingMetadataCount++; 126 this.fileManager_.metadataCache_.get(entry, 'filesystem', onProps); 127 } else if (entry.isDirectory) { 128 // Don't compute the directory size as it's expensive. 129 // crbug.com/179073. 130 this.showBytes = false; 131 break; 132 } 133 } 134 maybeDone(); 135 }; 136 137 /** 138 * Cancels any async computation by increasing the sequence number. Results 139 * of any previous call to computeBytes() will be discarded. 140 * 141 * @private 142 */ 143 FileSelection.prototype.cancelComputing_ = function() { 144 this.computeBytesSequence_++; 145 }; 146 147 /** 148 * This object encapsulates everything related to current selection. 149 * 150 * @param {FileManager} fileManager File manager instance. 151 * @extends {cr.EventTarget} 152 * @constructor 153 */ 154 function FileSelectionHandler(fileManager) { 155 this.fileManager_ = fileManager; 156 // TODO(dgozman): create a shared object with most of UI elements. 157 this.okButton_ = fileManager.okButton_; 158 this.filenameInput_ = fileManager.filenameInput_; 159 160 this.previewPanel_ = fileManager.dialogDom_.querySelector('.preview-panel'); 161 this.previewThumbnails_ = this.previewPanel_. 162 querySelector('.preview-thumbnails'); 163 this.previewSummary_ = this.previewPanel_.querySelector('.preview-summary'); 164 this.previewText_ = this.previewSummary_.querySelector('.preview-text'); 165 this.calculatingSize_ = this.previewSummary_. 166 querySelector('.calculating-size'); 167 this.calculatingSize_.textContent = str('CALCULATING_SIZE'); 168 169 this.searchBreadcrumbs_ = fileManager.searchBreadcrumbs_; 170 this.taskItems_ = fileManager.taskItems_; 171 172 this.animationTimeout_ = null; 173 } 174 175 /** 176 * FileSelectionHandler extends cr.EventTarget. 177 */ 178 FileSelectionHandler.prototype.__proto__ = cr.EventTarget.prototype; 179 180 /** 181 * Maximum amount of thumbnails in the preview pane. 182 * 183 * @const 184 * @type {number} 185 */ 186 FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT = 4; 187 188 /** 189 * Maximum width or height of an image what pops up when the mouse hovers 190 * thumbnail in the bottom panel (in pixels). 191 * 192 * @const 193 * @type {number} 194 */ 195 FileSelectionHandler.IMAGE_HOVER_PREVIEW_SIZE = 200; 196 197 /** 198 * Update the UI when the selection model changes. 199 * 200 * @param {cr.Event} event The change event. 201 */ 202 FileSelectionHandler.prototype.onFileSelectionChanged = function(event) { 203 var indexes = 204 this.fileManager_.getCurrentList().selectionModel.selectedIndexes; 205 if (this.selection) this.selection.cancelComputing_(); 206 var selection = new FileSelection(this.fileManager_, indexes); 207 this.selection = selection; 208 209 if (this.fileManager_.dialogType == DialogType.SELECT_SAVEAS_FILE) { 210 // If this is a save-as dialog, copy the selected file into the filename 211 // input text box. 212 if (this.selection.totalCount == 1 && 213 this.selection.entries[0].isFile && 214 this.filenameInput_.value != this.selection.entries[0].name) { 215 this.filenameInput_.value = this.selection.entries[0].name; 216 } 217 } 218 219 this.updateOkButton(); 220 221 if (this.selectionUpdateTimer_) { 222 clearTimeout(this.selectionUpdateTimer_); 223 this.selectionUpdateTimer_ = null; 224 } 225 226 this.hideCalculating_(); 227 228 // The rest of the selection properties are computed via (sometimes lengthy) 229 // asynchronous calls. We initiate these calls after a timeout. If the 230 // selection is changing quickly we only do this once when it slows down. 231 232 var updateDelay = 200; 233 var now = Date.now(); 234 if (now > (this.lastFileSelectionTime_ || 0) + updateDelay) { 235 // The previous selection change happened a while ago. Update the UI soon. 236 updateDelay = 0; 237 } 238 this.lastFileSelectionTime_ = now; 239 240 this.selectionUpdateTimer_ = setTimeout(function() { 241 this.selectionUpdateTimer_ = null; 242 if (this.selection == selection) 243 this.updateFileSelectionAsync(selection); 244 }.bind(this), updateDelay); 245 }; 246 247 /** 248 * Clears the primary UI selection elements. 249 */ 250 FileSelectionHandler.prototype.clearUI = function() { 251 this.previewThumbnails_.textContent = ''; 252 this.previewText_.textContent = ''; 253 this.hideCalculating_(); 254 this.taskItems_.hidden = true; 255 this.okButton_.disabled = true; 256 }; 257 258 /** 259 * Updates the Ok button enabled state. 260 * 261 * @return {boolean} Whether button is enabled. 262 */ 263 FileSelectionHandler.prototype.updateOkButton = function() { 264 var selectable; 265 var dialogType = this.fileManager_.dialogType; 266 267 if (dialogType == DialogType.SELECT_FOLDER || 268 dialogType == DialogType.SELECT_UPLOAD_FOLDER) { 269 // In SELECT_FOLDER mode, we allow to select current directory 270 // when nothing is selected. 271 selectable = this.selection.directoryCount <= 1 && 272 this.selection.fileCount == 0; 273 } else if (dialogType == DialogType.SELECT_OPEN_FILE) { 274 selectable = (this.isFileSelectionAvailable() && 275 this.selection.directoryCount == 0 && 276 this.selection.fileCount == 1); 277 } else if (dialogType == DialogType.SELECT_OPEN_MULTI_FILE) { 278 selectable = (this.isFileSelectionAvailable() && 279 this.selection.directoryCount == 0 && 280 this.selection.fileCount >= 1); 281 } else if (dialogType == DialogType.SELECT_SAVEAS_FILE) { 282 if (this.fileManager_.isOnReadonlyDirectory()) { 283 selectable = false; 284 } else { 285 selectable = !!this.filenameInput_.value; 286 } 287 } else if (dialogType == DialogType.FULL_PAGE) { 288 // No "select" buttons on the full page UI. 289 selectable = true; 290 } else { 291 throw new Error('Unknown dialog type'); 292 } 293 294 this.okButton_.disabled = !selectable; 295 return selectable; 296 }; 297 298 /** 299 * Check if all the files in the current selection are available. The only 300 * case when files might be not available is when the selection contains 301 * uncached Drive files and the browser is offline. 302 * 303 * @return {boolean} True if all files in the current selection are 304 * available. 305 */ 306 FileSelectionHandler.prototype.isFileSelectionAvailable = function() { 307 return !this.fileManager_.isOnDrive() || 308 !this.fileManager_.isDriveOffline() || 309 this.selection.allDriveFilesPresent; 310 }; 311 312 /** 313 * Sets the flag to force the preview panel hidden. 314 * @param {boolean} hidden True to force hidden. 315 */ 316 FileSelectionHandler.prototype.setPreviewPanelMustBeHidden = function(hidden) { 317 this.previewPanelMustBeHidden_ = hidden; 318 this.updatePreviewPanelVisibility_(); 319 }; 320 321 /** 322 * Animates preview panel show/hide transitions. 323 * 324 * @private 325 */ 326 FileSelectionHandler.prototype.updatePreviewPanelVisibility_ = function() { 327 var panel = this.previewPanel_; 328 var state = panel.getAttribute('visibility'); 329 var mustBeVisible = 330 // If one or more files are selected, show the file info. 331 (this.selection.totalCount > 0 || 332 // If the directory is not root dir, show the directory info. 333 !PathUtil.isRootPath(this.fileManager_.getCurrentDirectory()) || 334 // On Open File dialog, the preview panel is always shown. 335 this.fileManager_.dialogType == DialogType.SELECT_OPEN_FILE || 336 this.fileManager_.dialogType == DialogType.SELECT_OPEN_MULTI_FILE); 337 338 var stopHidingAndShow = function() { 339 clearTimeout(this.hidingTimeout_); 340 this.hidingTimeout_ = 0; 341 setVisibility('visible'); 342 }.bind(this); 343 344 var startHiding = function() { 345 setVisibility('hiding'); 346 this.hidingTimeout_ = setTimeout(function() { 347 this.hidingTimeout_ = 0; 348 setVisibility('hidden'); 349 cr.dispatchSimpleEvent(this, 'hide-preview-panel'); 350 }.bind(this), 250); 351 }.bind(this); 352 353 var show = function() { 354 setVisibility('visible'); 355 this.previewThumbnails_.textContent = ''; 356 cr.dispatchSimpleEvent(this, 'show-preview-panel'); 357 }.bind(this); 358 359 var setVisibility = function(visibility) { 360 panel.setAttribute('visibility', visibility); 361 }; 362 363 switch (state) { 364 case 'visible': 365 if (!mustBeVisible || this.previewPanelMustBeHidden_) 366 startHiding(); 367 break; 368 369 case 'hiding': 370 if (mustBeVisible && !this.previewPanelMustBeHidden_) 371 stopHidingAndShow(); 372 break; 373 374 case 'hidden': 375 if (mustBeVisible && !this.previewPanelMustBeHidden_) 376 show(); 377 } 378 }; 379 380 /** 381 * @return {boolean} True if space reserverd for the preview panel. 382 * @private 383 */ 384 FileSelectionHandler.prototype.isPreviewPanelVisibile_ = function() { 385 return this.previewPanel_.getAttribute('visibility') == 'visible'; 386 }; 387 388 /** 389 * Update the selection summary in preview panel. 390 * 391 * @private 392 */ 393 FileSelectionHandler.prototype.updatePreviewPanelText_ = function() { 394 var selection = this.selection; 395 if (selection.totalCount <= 1) { 396 // Hides the preview text if zero or one file is selected. We shows a 397 // breadcrumb list instead on the preview panel. 398 this.hideCalculating_(); 399 this.previewText_.textContent = ''; 400 return; 401 } 402 403 var text = ''; 404 if (selection.totalCount == 1) { 405 text = selection.entries[0].name; 406 } else if (selection.directoryCount == 0) { 407 text = strf('MANY_FILES_SELECTED', selection.fileCount); 408 } else if (selection.fileCount == 0) { 409 text = strf('MANY_DIRECTORIES_SELECTED', selection.directoryCount); 410 } else { 411 text = strf('MANY_ENTRIES_SELECTED', selection.totalCount); 412 } 413 414 if (selection.bytesKnown) { 415 this.hideCalculating_(); 416 if (selection.showBytes) { 417 var bytes = util.bytesToString(selection.bytes); 418 text += ', ' + bytes; 419 } 420 } else { 421 this.showCalculating_(); 422 } 423 424 this.previewText_.textContent = text; 425 }; 426 427 /** 428 * Displays the 'calculating size' label. 429 * 430 * @private 431 */ 432 FileSelectionHandler.prototype.showCalculating_ = function() { 433 if (this.animationTimeout_) { 434 clearTimeout(this.animationTimeout_); 435 this.animationTimeout_ = null; 436 } 437 438 var dotCount = 0; 439 440 var advance = function() { 441 this.animationTimeout_ = setTimeout(advance, 1000); 442 443 var s = this.calculatingSize_.textContent; 444 s = s.replace(/(\.)+$/, ''); 445 for (var i = 0; i < dotCount; i++) { 446 s += '.'; 447 } 448 this.calculatingSize_.textContent = s; 449 450 dotCount = (dotCount + 1) % 3; 451 }.bind(this); 452 453 var start = function() { 454 this.calculatingSize_.hidden = false; 455 advance(); 456 }.bind(this); 457 458 this.animationTimeout_ = setTimeout(start, 500); 459 }; 460 461 /** 462 * Hides the 'calculating size' label. 463 * 464 * @private 465 */ 466 FileSelectionHandler.prototype.hideCalculating_ = function() { 467 if (this.animationTimeout_) { 468 clearTimeout(this.animationTimeout_); 469 this.animationTimeout_ = null; 470 } 471 this.calculatingSize_.hidden = true; 472 }; 473 474 /** 475 * Calculates async selection stats and updates secondary UI elements. 476 * 477 * @param {FileSelection} selection The selection object. 478 */ 479 FileSelectionHandler.prototype.updateFileSelectionAsync = function(selection) { 480 if (this.selection != selection) return; 481 482 // Update the file tasks. 483 if (this.fileManager_.dialogType == DialogType.FULL_PAGE && 484 selection.directoryCount == 0 && selection.fileCount > 0) { 485 selection.createTasks(function() { 486 if (this.selection != selection) 487 return; 488 selection.tasks.display(this.taskItems_); 489 selection.tasks.updateMenuItem(); 490 }.bind(this)); 491 } else { 492 this.taskItems_.hidden = true; 493 } 494 495 // Update preview panels. 496 var wasVisible = this.isPreviewPanelVisibile_(); 497 var thumbnailEntries; 498 if (selection.totalCount == 0) { 499 thumbnailEntries = [ 500 this.fileManager_.getCurrentDirectoryEntry() 501 ]; 502 } else { 503 thumbnailEntries = selection.entries; 504 if (selection.totalCount != 1) { 505 selection.computeBytes(function() { 506 if (this.selection != selection) 507 return; 508 this.updatePreviewPanelText_(); 509 }.bind(this)); 510 } 511 } 512 this.updatePreviewPanelVisibility_(); 513 this.updatePreviewPanelText_(); 514 this.showPreviewThumbnails_(thumbnailEntries); 515 516 // Update breadcrums. 517 var updateTarget = null; 518 var path = this.fileManager_.getCurrentDirectory(); 519 if (selection.totalCount == 1) { 520 // Shows the breadcrumb list when a file is selected. 521 updateTarget = selection.entries[0].fullPath; 522 } else if (selection.totalCount == 0 && 523 this.isPreviewPanelVisibile_()) { 524 // Shows the breadcrumb list when no file is selected and the preview 525 // panel is visible. 526 updateTarget = path; 527 } 528 this.updatePreviewPanelBreadcrumbs_(updateTarget); 529 530 // Scroll to item 531 if (!wasVisible && this.selection.totalCount == 1) { 532 var list = this.fileManager_.getCurrentList(); 533 list.scrollIndexIntoView(list.selectionModel.selectedIndex); 534 } 535 536 // Sync the commands availability. 537 if (selection.totalCount != 0) 538 this.fileManager_.updateCommands(); 539 540 // Update context menu. 541 this.fileManager_.updateContextMenuActionItems(null, false); 542 543 // Inform tests it's OK to click buttons now. 544 if (selection.totalCount > 0) { 545 chrome.test.sendMessage('selection-change-complete'); 546 } 547 }; 548 549 /** 550 * Renders preview thumbnails in preview panel. 551 * 552 * @param {Array.<FileEntry>} entries The entries of selected object. 553 * @private 554 */ 555 FileSelectionHandler.prototype.showPreviewThumbnails_ = function(entries) { 556 var selection = this.selection; 557 var thumbnails = []; 558 var thumbnailCount = 0; 559 var thumbnailLoaded = -1; 560 var forcedShowTimeout = null; 561 var thumbnailsHaveZoom = false; 562 var self = this; 563 564 var showThumbnails = function() { 565 // have-zoom class may be updated twice: then timeout exceeds and then 566 // then all images loaded. 567 if (self.selection == selection) { 568 if (thumbnailsHaveZoom) { 569 self.previewThumbnails_.classList.add('has-zoom'); 570 } else { 571 self.previewThumbnails_.classList.remove('has-zoom'); 572 } 573 } 574 575 if (forcedShowTimeout === null) 576 return; 577 clearTimeout(forcedShowTimeout); 578 forcedShowTimeout = null; 579 580 // FileSelection could change while images are loading. 581 if (self.selection == selection) { 582 self.previewThumbnails_.textContent = ''; 583 for (var i = 0; i < thumbnails.length; i++) 584 self.previewThumbnails_.appendChild(thumbnails[i]); 585 } 586 }; 587 588 var onThumbnailLoaded = function() { 589 thumbnailLoaded++; 590 if (thumbnailLoaded == thumbnailCount) 591 showThumbnails(); 592 }; 593 594 var thumbnailClickHandler = function() { 595 if (selection.tasks) 596 selection.tasks.executeDefault(); 597 }; 598 599 var doc = this.fileManager_.document_; 600 for (var i = 0; i < entries.length; i++) { 601 var entry = entries[i]; 602 603 if (thumbnailCount < FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT) { 604 var box = doc.createElement('div'); 605 box.className = 'thumbnail'; 606 if (thumbnailCount == 0) { 607 var zoomed = doc.createElement('div'); 608 zoomed.hidden = true; 609 thumbnails.push(zoomed); 610 var onFirstThumbnailLoaded = function(img, transform) { 611 if (img && self.decorateThumbnailZoom_(zoomed, img, transform)) { 612 zoomed.hidden = false; 613 thumbnailsHaveZoom = true; 614 } 615 onThumbnailLoaded(); 616 }; 617 var thumbnail = this.renderThumbnail_(entry, onFirstThumbnailLoaded); 618 zoomed.addEventListener('click', thumbnailClickHandler); 619 } else { 620 var thumbnail = this.renderThumbnail_(entry, onThumbnailLoaded); 621 } 622 thumbnailCount++; 623 box.appendChild(thumbnail); 624 box.style.zIndex = 625 FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT + 1 - i; 626 box.addEventListener('click', thumbnailClickHandler); 627 628 thumbnails.push(box); 629 } 630 } 631 632 forcedShowTimeout = setTimeout(showThumbnails, 633 FileManager.THUMBNAIL_SHOW_DELAY); 634 onThumbnailLoaded(); 635 }; 636 637 /** 638 * Renders a thumbnail for the buttom panel. 639 * 640 * @param {Entry} entry Entry to render for. 641 * @param {function} callback Called when image loaded. 642 * @return {HTMLDivElement} Created element. 643 * @private 644 */ 645 FileSelectionHandler.prototype.renderThumbnail_ = function(entry, callback) { 646 var thumbnail = this.fileManager_.document_.createElement('div'); 647 FileGrid.decorateThumbnailBox(thumbnail, 648 entry, 649 this.fileManager_.metadataCache_, 650 ThumbnailLoader.FillMode.FILL, 651 FileGrid.ThumbnailQuality.LOW, 652 callback); 653 return thumbnail; 654 }; 655 656 /** 657 * Updates the breadcrumbs in the preview panel. 658 * 659 * @param {?string} path Path to be shown in the breadcrumbs list 660 * @private 661 */ 662 FileSelectionHandler.prototype.updatePreviewPanelBreadcrumbs_ = function(path) { 663 if (!path) 664 this.searchBreadcrumbs_.hide(); 665 else 666 this.searchBreadcrumbs_.show(PathUtil.getRootPath(path), path); 667 }; 668 669 /** 670 * Updates the search breadcrumbs. This method should not be used in the new ui. 671 * 672 * @private 673 */ 674 FileSelectionHandler.prototype.updateSearchBreadcrumbs_ = function() { 675 var selectedIndexes = 676 this.fileManager_.getCurrentList().selectionModel.selectedIndexes; 677 if (selectedIndexes.length !== 1 || 678 !this.fileManager_.directoryModel_.isSearching()) { 679 this.searchBreadcrumbs_.hide(); 680 return; 681 } 682 683 var entry = this.fileManager_.getFileList().item( 684 selectedIndexes[0]); 685 this.searchBreadcrumbs_.show( 686 PathUtil.getRootPath(entry.fullPath), 687 entry.fullPath); 688 }; 689 690 /** 691 * Creates enlarged image for a bottom pannel thumbnail. 692 * Image's assumed to be just loaded and not inserted into the DOM. 693 * 694 * @param {HTMLElement} largeImageBox DIV element to decorate. 695 * @param {HTMLElement} img Loaded image. 696 * @param {Object} transform Image transformation description. 697 * @return {boolean} True if zoomed image is present. 698 * @private 699 */ 700 FileSelectionHandler.prototype.decorateThumbnailZoom_ = function( 701 largeImageBox, img, transform) { 702 var width = img.width; 703 var height = img.height; 704 var THUMBNAIL_SIZE = 35; 705 if (width < THUMBNAIL_SIZE * 2 && height < THUMBNAIL_SIZE * 2) 706 return false; 707 708 var scale = Math.min(1, 709 FileSelectionHandler.IMAGE_HOVER_PREVIEW_SIZE / Math.max(width, height)); 710 711 var imageWidth = Math.round(width * scale); 712 var imageHeight = Math.round(height * scale); 713 714 var largeImage = this.fileManager_.document_.createElement('img'); 715 if (scale < 0.3) { 716 // Scaling large images kills animation. Downscale it in advance. 717 718 // Canvas scales images with liner interpolation. Make a larger 719 // image (but small enough to not kill animation) and let IMG 720 // scale it smoothly. 721 var INTERMEDIATE_SCALE = 3; 722 var canvas = this.fileManager_.document_.createElement('canvas'); 723 canvas.width = imageWidth * INTERMEDIATE_SCALE; 724 canvas.height = imageHeight * INTERMEDIATE_SCALE; 725 var ctx = canvas.getContext('2d'); 726 ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 727 // Using bigger than default compression reduces image size by 728 // several times. Quality degradation compensated by greater resolution. 729 largeImage.src = canvas.toDataURL('image/jpeg', 0.6); 730 } else { 731 largeImage.src = img.src; 732 } 733 largeImageBox.className = 'popup'; 734 735 var boxWidth = Math.max(THUMBNAIL_SIZE, imageWidth); 736 var boxHeight = Math.max(THUMBNAIL_SIZE, imageHeight); 737 738 if (transform && transform.rotate90 % 2 == 1) { 739 var t = boxWidth; 740 boxWidth = boxHeight; 741 boxHeight = t; 742 } 743 744 var style = largeImageBox.style; 745 style.width = boxWidth + 'px'; 746 style.height = boxHeight + 'px'; 747 style.top = (-boxHeight + THUMBNAIL_SIZE) + 'px'; 748 749 var style = largeImage.style; 750 style.width = imageWidth + 'px'; 751 style.height = imageHeight + 'px'; 752 style.left = (boxWidth - imageWidth) / 2 + 'px'; 753 style.top = (boxHeight - imageHeight) / 2 + 'px'; 754 style.position = 'relative'; 755 756 util.applyTransform(largeImage, transform); 757 758 largeImageBox.appendChild(largeImage); 759 largeImageBox.style.zIndex = 1000; 760 return true; 761 }; 762