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 * Global (placed in the window object) variable name to hold internal 9 * file dragging information. Needed to show visual feedback while dragging 10 * since DataTransfer object is in protected state. Reachable from other 11 * file manager instances. 12 */ 13var DRAG_AND_DROP_GLOBAL_DATA = '__drag_and_drop_global_data'; 14 15/** 16 * @param {HTMLDocument} doc Owning document. 17 * @param {FileOperationManager} fileOperationManager File operation manager 18 * instance. 19 * @param {MetadataCache} metadataCache Metadata cache service. 20 * @param {DirectoryModel} directoryModel Directory model instance. 21 * @param {VolumeManagerWrapper} volumeManager Volume manager instance. 22 * @param {MultiProfileShareDialog} multiProfileShareDialog Share dialog to be 23 * used to share files from another profile. 24 * @constructor 25 */ 26function FileTransferController(doc, 27 fileOperationManager, 28 metadataCache, 29 directoryModel, 30 volumeManager, 31 multiProfileShareDialog) { 32 this.document_ = doc; 33 this.fileOperationManager_ = fileOperationManager; 34 this.metadataCache_ = metadataCache; 35 this.directoryModel_ = directoryModel; 36 this.volumeManager_ = volumeManager; 37 this.multiProfileShareDialog_ = multiProfileShareDialog; 38 39 this.directoryModel_.getFileList().addEventListener( 40 'change', function(event) { 41 if (this.directoryModel_.getFileListSelection(). 42 getIndexSelected(event.index)) { 43 this.onSelectionChanged_(); 44 } 45 }.bind(this)); 46 this.directoryModel_.getFileListSelection().addEventListener('change', 47 this.onSelectionChanged_.bind(this)); 48 49 /** 50 * Promise to be fulfilled with the thumbnail image of selected file in drag 51 * operation. Used if only one element is selected. 52 * @type {Promise} 53 * @private 54 */ 55 this.preloadedThumbnailImagePromise_ = null; 56 57 /** 58 * File objects for selected files. 59 * 60 * @type {Array.<File>} 61 * @private 62 */ 63 this.selectedFileObjects_ = []; 64 65 /** 66 * Drag selector. 67 * @type {DragSelector} 68 * @private 69 */ 70 this.dragSelector_ = new DragSelector(); 71 72 /** 73 * Whether a user is touching the device or not. 74 * @type {boolean} 75 * @private 76 */ 77 this.touching_ = false; 78} 79 80/** 81 * Size of drag thumbnail for image files. 82 * 83 * @type {number} 84 * @const 85 * @private 86 */ 87FileTransferController.DRAG_THUMBNAIL_SIZE_ = 64; 88 89FileTransferController.prototype = { 90 __proto__: cr.EventTarget.prototype, 91 92 /** 93 * @this {FileTransferController} 94 * @param {cr.ui.List} list Items in the list will be draggable. 95 */ 96 attachDragSource: function(list) { 97 list.style.webkitUserDrag = 'element'; 98 list.addEventListener('dragstart', this.onDragStart_.bind(this, list)); 99 list.addEventListener('dragend', this.onDragEnd_.bind(this, list)); 100 list.addEventListener('touchstart', this.onTouchStart_.bind(this)); 101 list.ownerDocument.addEventListener( 102 'touchend', this.onTouchEnd_.bind(this), true); 103 list.ownerDocument.addEventListener( 104 'touchcancel', this.onTouchEnd_.bind(this), true); 105 }, 106 107 /** 108 * @this {FileTransferController} 109 * @param {cr.ui.List} list List itself and its directory items will could 110 * be drop target. 111 * @param {boolean=} opt_onlyIntoDirectories If true only directory list 112 * items could be drop targets. Otherwise any other place of the list 113 * accetps files (putting it into the current directory). 114 */ 115 attachFileListDropTarget: function(list, opt_onlyIntoDirectories) { 116 list.addEventListener('dragover', this.onDragOver_.bind(this, 117 !!opt_onlyIntoDirectories, list)); 118 list.addEventListener('dragenter', 119 this.onDragEnterFileList_.bind(this, list)); 120 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); 121 list.addEventListener('drop', 122 this.onDrop_.bind(this, !!opt_onlyIntoDirectories)); 123 }, 124 125 /** 126 * @this {FileTransferController} 127 * @param {DirectoryTree} tree Its sub items will could be drop target. 128 */ 129 attachTreeDropTarget: function(tree) { 130 tree.addEventListener('dragover', this.onDragOver_.bind(this, true, tree)); 131 tree.addEventListener('dragenter', this.onDragEnterTree_.bind(this, tree)); 132 tree.addEventListener('dragleave', this.onDragLeave_.bind(this, tree)); 133 tree.addEventListener('drop', this.onDrop_.bind(this, true)); 134 }, 135 136 /** 137 * @this {FileTransferController} 138 * @param {NavigationList} tree Its sub items will could be drop target. 139 */ 140 attachNavigationListDropTarget: function(list) { 141 list.addEventListener('dragover', 142 this.onDragOver_.bind(this, true /* onlyIntoDirectories */, list)); 143 list.addEventListener('dragenter', 144 this.onDragEnterVolumesList_.bind(this, list)); 145 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); 146 list.addEventListener('drop', 147 this.onDrop_.bind(this, true /* onlyIntoDirectories */)); 148 }, 149 150 /** 151 * Attach handlers of copy, cut and paste operations to the document. 152 * 153 * @this {FileTransferController} 154 */ 155 attachCopyPasteHandlers: function() { 156 this.document_.addEventListener('beforecopy', 157 this.onBeforeCopy_.bind(this)); 158 this.document_.addEventListener('copy', 159 this.onCopy_.bind(this)); 160 this.document_.addEventListener('beforecut', 161 this.onBeforeCut_.bind(this)); 162 this.document_.addEventListener('cut', 163 this.onCut_.bind(this)); 164 this.document_.addEventListener('beforepaste', 165 this.onBeforePaste_.bind(this)); 166 this.document_.addEventListener('paste', 167 this.onPaste_.bind(this)); 168 this.copyCommand_ = this.document_.querySelector('command#copy'); 169 }, 170 171 /** 172 * Write the current selection to system clipboard. 173 * 174 * @this {FileTransferController} 175 * @param {DataTransfer} dataTransfer DataTransfer from the event. 176 * @param {string} effectAllowed Value must be valid for the 177 * |dataTransfer.effectAllowed| property. 178 */ 179 cutOrCopy_: function(dataTransfer, effectAllowed) { 180 // Existence of the volumeInfo is checked in canXXX methods. 181 var volumeInfo = this.volumeManager_.getVolumeInfo( 182 this.currentDirectoryContentEntry); 183 // Tag to check it's filemanager data. 184 dataTransfer.setData('fs/tag', 'filemanager-data'); 185 dataTransfer.setData('fs/sourceRootURL', 186 volumeInfo.fileSystem.root.toURL()); 187 var sourceURLs = util.entriesToURLs(this.selectedEntries_); 188 dataTransfer.setData('fs/sources', sourceURLs.join('\n')); 189 dataTransfer.effectAllowed = effectAllowed; 190 dataTransfer.setData('fs/effectallowed', effectAllowed); 191 dataTransfer.setData('fs/missingFileContents', 192 !this.isAllSelectedFilesAvailable_()); 193 194 for (var i = 0; i < this.selectedFileObjects_.length; i++) { 195 dataTransfer.items.add(this.selectedFileObjects_[i]); 196 } 197 }, 198 199 /** 200 * @this {FileTransferController} 201 * @return {Object.<string, string>} Drag and drop global data object. 202 */ 203 getDragAndDropGlobalData_: function() { 204 if (window[DRAG_AND_DROP_GLOBAL_DATA]) 205 return window[DRAG_AND_DROP_GLOBAL_DATA]; 206 207 // Dragging from other tabs/windows. 208 var views = chrome && chrome.extension ? chrome.extension.getViews() : []; 209 for (var i = 0; i < views.length; i++) { 210 if (views[i][DRAG_AND_DROP_GLOBAL_DATA]) 211 return views[i][DRAG_AND_DROP_GLOBAL_DATA]; 212 } 213 return null; 214 }, 215 216 /** 217 * Extracts source root URL from the |dataTransfer| object. 218 * 219 * @this {FileTransferController} 220 * @param {DataTransfer} dataTransfer DataTransfer object from the event. 221 * @return {string} URL or an empty string (if unknown). 222 */ 223 getSourceRootURL_: function(dataTransfer) { 224 var sourceRootURL = dataTransfer.getData('fs/sourceRootURL'); 225 if (sourceRootURL) 226 return sourceRootURL; 227 228 // |dataTransfer| in protected mode. 229 var globalData = this.getDragAndDropGlobalData_(); 230 if (globalData) 231 return globalData.sourceRootURL; 232 233 // Unknown source. 234 return ''; 235 }, 236 237 /** 238 * @this {FileTransferController} 239 * @param {DataTransfer} dataTransfer DataTransfer object from the event. 240 * @return {boolean} Returns true when missing some file contents. 241 */ 242 isMissingFileContents_: function(dataTransfer) { 243 var data = dataTransfer.getData('fs/missingFileContents'); 244 if (!data) { 245 // |dataTransfer| in protected mode. 246 var globalData = this.getDragAndDropGlobalData_(); 247 if (globalData) 248 data = globalData.missingFileContents; 249 } 250 return data === 'true'; 251 }, 252 253 /** 254 * Obtains entries that need to share with me. 255 * The method also observers child entries of the given entries. 256 * @param {Array.<Entries>} entries Entries. 257 * @return {Promise} Promise to be fulfilled with the entries that need to 258 * share. 259 */ 260 getMultiProfileShareEntries_: function(entries) { 261 // Utility function to concat arrays. 262 var concatArrays = function(arrays) { 263 return Array.prototype.concat.apply([], arrays); 264 }; 265 266 // Call processEntry for each item of entries. 267 var processEntries = function(entries) { 268 var files = entries.filter(function(entry) {return entry.isFile;}); 269 var dirs = entries.filter(function(entry) {return !entry.isFile;}); 270 var promises = dirs.map(processDirectoryEntry); 271 if (files.length > 0) 272 promises.push(processFileEntries(files)); 273 return Promise.all(promises).then(concatArrays); 274 }; 275 276 // Check all file entries and keeps only those need sharing operation. 277 var processFileEntries = function(entries) { 278 return new Promise(function(callback) { 279 var urls = util.entriesToURLs(entries); 280 chrome.fileBrowserPrivate.getDriveEntryProperties(urls, callback); 281 }). 282 then(function(metadatas) { 283 return entries.filter(function(entry, i) { 284 var metadata = metadatas[i]; 285 return metadata && metadata.isHosted && !metadata.sharedWithMe; 286 }); 287 }); 288 }; 289 290 // Check child entries. 291 var processDirectoryEntry = function(entry) { 292 return readEntries(entry.createReader()); 293 }; 294 295 // Read entries from DirectoryReader and call processEntries for the chunk 296 // of entries. 297 var readEntries = function(reader) { 298 return new Promise(reader.readEntries.bind(reader)).then( 299 function(entries) { 300 if (entries.length > 0) { 301 return Promise.all( 302 [processEntries(entries), readEntries(reader)]). 303 then(concatArrays); 304 } else { 305 return []; 306 } 307 }, 308 function(error) { 309 console.warn( 310 'Error happens while reading directory.', error); 311 return []; 312 }); 313 }.bind(this); 314 315 // Filter entries that is owned by the current user, and call 316 // processEntries. 317 return processEntries(entries.filter(function(entry) { 318 // If the volumeInfo is found, the entry belongs to the current user. 319 return !this.volumeManager_.getVolumeInfo(entry); 320 }.bind(this))); 321 }, 322 323 /** 324 * Queue up a file copy operation based on the current system clipboard. 325 * 326 * @this {FileTransferController} 327 * @param {DataTransfer} dataTransfer System data transfer object. 328 * @param {DirectoryEntry=} opt_destinationEntry Paste destination. 329 * @param {string=} opt_effect Desired drop/paste effect. Could be 330 * 'move'|'copy' (default is copy). Ignored if conflicts with 331 * |dataTransfer.effectAllowed|. 332 * @return {string} Either "copy" or "move". 333 */ 334 paste: function(dataTransfer, opt_destinationEntry, opt_effect) { 335 var sourceURLs = dataTransfer.getData('fs/sources') ? 336 dataTransfer.getData('fs/sources').split('\n') : []; 337 // effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers 338 // work fine. 339 var effectAllowed = dataTransfer.effectAllowed !== 'uninitialized' ? 340 dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed'); 341 var toMove = util.isDropEffectAllowed(effectAllowed, 'move') && 342 (!util.isDropEffectAllowed(effectAllowed, 'copy') || 343 opt_effect === 'move'); 344 var destinationEntry = 345 opt_destinationEntry || this.currentDirectoryContentEntry; 346 var entries; 347 var failureUrls; 348 349 util.URLsToEntries(sourceURLs). 350 then(function(result) { 351 entries = result.entries; 352 failureUrls = result.failureUrls; 353 // Check if cross share is needed or not. 354 return this.getMultiProfileShareEntries_(entries); 355 }.bind(this)). 356 then(function(shareEntries) { 357 if (shareEntries.length === 0) 358 return; 359 return this.multiProfileShareDialog_.show(shareEntries.length > 1). 360 then(function(dialogResult) { 361 if (dialogResult === 'cancel') 362 return Promise.reject('ABORT'); 363 // Do cross share. 364 // TODO(hirono): Make the loop cancellable. 365 var requestDriveShare = function(index) { 366 if (index >= shareEntries.length) 367 return; 368 return new Promise(function(fulfill) { 369 chrome.fileBrowserPrivate.requestDriveShare( 370 shareEntries[index].toURL(), 371 dialogResult, 372 function() { 373 // TODO(hirono): Check chrome.runtime.lastError here. 374 fulfill(); 375 }); 376 }).then(requestDriveShare.bind(null, index + 1)); 377 }; 378 return requestDriveShare(0); 379 }); 380 }.bind(this)). 381 then(function() { 382 // Start the pasting operation. 383 this.fileOperationManager_.paste( 384 entries, destinationEntry, toMove); 385 386 // Publish events for failureUrls. 387 for (var i = 0; i < failureUrls.length; i++) { 388 var fileName = 389 decodeURIComponent(failureUrls[i].replace(/^.+\//, '')); 390 var event = new Event('source-not-found'); 391 event.fileName = fileName; 392 event.progressType = 393 toMove ? ProgressItemType.MOVE : ProgressItemType.COPY; 394 this.dispatchEvent(event); 395 } 396 }.bind(this)). 397 catch(function(error) { 398 if (error !== 'ABORT') 399 console.error(error.stack ? error.stack : error); 400 }); 401 return toMove ? 'move' : 'copy'; 402 }, 403 404 /** 405 * Preloads an image thumbnail for the specified file entry. 406 * 407 * @this {FileTransferController} 408 * @param {Entry} entry Entry to preload a thumbnail for. 409 */ 410 preloadThumbnailImage_: function(entry) { 411 var metadataPromise = new Promise(function(fulfill, reject) { 412 this.metadataCache_.getOne(entry, 413 'thumbnail|filesystem', 414 function(metadata) { 415 if (metadata) 416 fulfill(metadata); 417 else 418 reject('Failed to fetch metadata.'); 419 }); 420 }.bind(this)); 421 422 var imagePromise = metadataPromise.then(function(metadata) { 423 return new Promise(function(fulfill, reject) { 424 var loader = new ThumbnailLoader( 425 entry, ThumbnailLoader.LoaderType.Image, metadata); 426 loader.loadDetachedImage(function(result) { 427 if (result) 428 fulfill(loader.getImage()); 429 }); 430 }); 431 }); 432 433 imagePromise.then(function(image) { 434 // Store the image so that we can obtain the image synchronously. 435 imagePromise.value = image; 436 }, function(error) { 437 console.error(error.stack || error); 438 }); 439 440 this.preloadedThumbnailImagePromise_ = imagePromise; 441 }, 442 443 /** 444 * Renders a drag-and-drop thumbnail. 445 * 446 * @this {FileTransferController} 447 * @return {HTMLElement} Element containing the thumbnail. 448 */ 449 renderThumbnail_: function() { 450 var length = this.selectedEntries_.length; 451 452 var container = this.document_.querySelector('#drag-container'); 453 var contents = this.document_.createElement('div'); 454 contents.className = 'drag-contents'; 455 container.appendChild(contents); 456 457 // Option 1. Multiple selection, render only a label. 458 if (length > 1) { 459 var label = this.document_.createElement('div'); 460 label.className = 'label'; 461 label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length); 462 contents.appendChild(label); 463 return container; 464 } 465 466 // Option 2. Thumbnail image available, then render it without 467 // a label. 468 if (this.preloadedThumbnailImagePromise_ && 469 this.preloadedThumbnailImagePromise_.value) { 470 var thumbnailImage = this.preloadedThumbnailImagePromise_.value; 471 472 // Resize the image to canvas. 473 var canvas = document.createElement('canvas'); 474 canvas.width = FileTransferController.DRAG_THUMBNAIL_SIZE_; 475 canvas.height = FileTransferController.DRAG_THUMBNAIL_SIZE_; 476 477 var minScale = Math.min( 478 thumbnailImage.width / canvas.width, 479 thumbnailImage.height / canvas.height); 480 var srcWidth = Math.min(canvas.width * minScale, thumbnailImage.width); 481 var srcHeight = Math.min(canvas.height * minScale, thumbnailImage.height); 482 483 var context = canvas.getContext('2d'); 484 context.drawImage(thumbnailImage, 485 (thumbnailImage.width - srcWidth) / 2, 486 (thumbnailImage.height - srcHeight) / 2, 487 srcWidth, 488 srcHeight, 489 0, 490 0, 491 canvas.width, 492 canvas.height); 493 contents.classList.add('for-image'); 494 contents.appendChild(canvas); 495 return container; 496 } 497 498 // Option 3. Thumbnail not available. Render an icon and a label. 499 var entry = this.selectedEntries_[0]; 500 var icon = this.document_.createElement('div'); 501 icon.className = 'detail-icon'; 502 icon.setAttribute('file-type-icon', FileType.getIcon(entry)); 503 contents.appendChild(icon); 504 var label = this.document_.createElement('div'); 505 label.className = 'label'; 506 label.textContent = entry.name; 507 contents.appendChild(label); 508 return container; 509 }, 510 511 /** 512 * @this {FileTransferController} 513 * @param {cr.ui.List} list Drop target list 514 * @param {Event} event A dragstart event of DOM. 515 */ 516 onDragStart_: function(list, event) { 517 // Check if a drag selection should be initiated or not. 518 if (list.shouldStartDragSelection(event)) { 519 event.preventDefault(); 520 // If this drag operation is initiated by mouse, start selecting area. 521 if (!this.touching_) 522 this.dragSelector_.startDragSelection(list, event); 523 return; 524 } 525 526 // Nothing selected. 527 if (!this.selectedEntries_.length) { 528 event.preventDefault(); 529 return; 530 } 531 532 var dt = event.dataTransfer; 533 var canCopy = this.canCopyOrDrag_(dt); 534 var canCut = this.canCutOrDrag_(dt); 535 if (canCopy || canCut) { 536 if (canCopy && canCut) { 537 this.cutOrCopy_(dt, 'all'); 538 } else if (canCopy) { 539 this.cutOrCopy_(dt, 'copyLink'); 540 } else { 541 this.cutOrCopy_(dt, 'move'); 542 } 543 } else { 544 event.preventDefault(); 545 return; 546 } 547 548 var dragThumbnail = this.renderThumbnail_(); 549 dt.setDragImage(dragThumbnail, 0, 0); 550 551 window[DRAG_AND_DROP_GLOBAL_DATA] = { 552 sourceRootURL: dt.getData('fs/sourceRootURL'), 553 missingFileContents: dt.getData('fs/missingFileContents'), 554 }; 555 }, 556 557 /** 558 * @this {FileTransferController} 559 * @param {cr.ui.List} list Drop target list. 560 * @param {Event} event A dragend event of DOM. 561 */ 562 onDragEnd_: function(list, event) { 563 // TODO(fukino): This is workaround for crbug.com/373125. 564 // This should be removed after the bug is fixed. 565 this.touching_ = false; 566 567 var container = this.document_.querySelector('#drag-container'); 568 container.textContent = ''; 569 this.clearDropTarget_(); 570 delete window[DRAG_AND_DROP_GLOBAL_DATA]; 571 }, 572 573 /** 574 * @this {FileTransferController} 575 * @param {boolean} onlyIntoDirectories True if the drag is only into 576 * directories. 577 * @param {cr.ui.List} list Drop target list. 578 * @param {Event} event A dragover event of DOM. 579 */ 580 onDragOver_: function(onlyIntoDirectories, list, event) { 581 event.preventDefault(); 582 var entry = this.destinationEntry_ || 583 (!onlyIntoDirectories && this.currentDirectoryContentEntry); 584 event.dataTransfer.dropEffect = this.selectDropEffect_(event, entry); 585 event.preventDefault(); 586 }, 587 588 /** 589 * @this {FileTransferController} 590 * @param {cr.ui.List} list Drop target list. 591 * @param {Event} event A dragenter event of DOM. 592 */ 593 onDragEnterFileList_: function(list, event) { 594 event.preventDefault(); // Required to prevent the cursor flicker. 595 this.lastEnteredTarget_ = event.target; 596 var item = list.getListItemAncestor(event.target); 597 item = item && list.isItem(item) ? item : null; 598 if (item === this.dropTarget_) 599 return; 600 601 var entry = item && list.dataModel.item(item.listIndex); 602 if (entry) 603 this.setDropTarget_(item, event.dataTransfer, entry); 604 else 605 this.clearDropTarget_(); 606 }, 607 608 /** 609 * @this {FileTransferController} 610 * @param {DirectoryTree} tree Drop target tree. 611 * @param {Event} event A dragenter event of DOM. 612 */ 613 onDragEnterTree_: function(tree, event) { 614 event.preventDefault(); // Required to prevent the cursor flicker. 615 this.lastEnteredTarget_ = event.target; 616 var item = event.target; 617 while (item && !(item instanceof DirectoryItem)) { 618 item = item.parentNode; 619 } 620 621 if (item === this.dropTarget_) 622 return; 623 624 var entry = item && item.entry; 625 if (entry) { 626 this.setDropTarget_(item, event.dataTransfer, entry); 627 } else { 628 this.clearDropTarget_(); 629 } 630 }, 631 632 /** 633 * @this {FileTransferController} 634 * @param {NavigationList} list Drop target list. 635 * @param {Event} event A dragenter event of DOM. 636 */ 637 onDragEnterVolumesList_: function(list, event) { 638 event.preventDefault(); // Required to prevent the cursor flicker. 639 640 this.lastEnteredTarget_ = event.target; 641 var item = list.getListItemAncestor(event.target); 642 item = item && list.isItem(item) ? item : null; 643 if (item === this.dropTarget_) 644 return; 645 646 var modelItem = item && list.dataModel.item(item.listIndex); 647 if (modelItem && modelItem.isShortcut) { 648 this.setDropTarget_(item, event.dataTransfer, modelItem.entry); 649 return; 650 } 651 if (modelItem && modelItem.isVolume && modelItem.volumeInfo.displayRoot) { 652 this.setDropTarget_( 653 item, event.dataTransfer, modelItem.volumeInfo.displayRoot); 654 return; 655 } 656 657 this.clearDropTarget_(); 658 }, 659 660 /** 661 * @this {FileTransferController} 662 * @param {cr.ui.List} list Drop target list. 663 * @param {Event} event A dragleave event of DOM. 664 */ 665 onDragLeave_: function(list, event) { 666 // If mouse moves from one element to another the 'dragenter' 667 // event for the new element comes before the 'dragleave' event for 668 // the old one. In this case event.target !== this.lastEnteredTarget_ 669 // and handler of the 'dragenter' event has already caried of 670 // drop target. So event.target === this.lastEnteredTarget_ 671 // could only be if mouse goes out of listened element. 672 if (event.target === this.lastEnteredTarget_) { 673 this.clearDropTarget_(); 674 this.lastEnteredTarget_ = null; 675 } 676 }, 677 678 /** 679 * @this {FileTransferController} 680 * @param {boolean} onlyIntoDirectories True if the drag is only into 681 * directories. 682 * @param {Event} event A dragleave event of DOM. 683 */ 684 onDrop_: function(onlyIntoDirectories, event) { 685 if (onlyIntoDirectories && !this.dropTarget_) 686 return; 687 var destinationEntry = this.destinationEntry_ || 688 this.currentDirectoryContentEntry; 689 if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry)) 690 return; 691 event.preventDefault(); 692 this.paste(event.dataTransfer, destinationEntry, 693 this.selectDropEffect_(event, destinationEntry)); 694 this.clearDropTarget_(); 695 }, 696 697 /** 698 * Sets the drop target. 699 * 700 * @this {FileTransferController} 701 * @param {Element} domElement Target of the drop. 702 * @param {DataTransfer} dataTransfer Data transfer object. 703 * @param {DirectoryEntry} destinationEntry Destination entry. 704 */ 705 setDropTarget_: function(domElement, dataTransfer, destinationEntry) { 706 if (this.dropTarget_ === domElement) 707 return; 708 709 // Remove the old drop target. 710 this.clearDropTarget_(); 711 712 // Set the new drop target. 713 this.dropTarget_ = domElement; 714 715 if (!domElement || 716 !destinationEntry.isDirectory || 717 !this.canPasteOrDrop_(dataTransfer, destinationEntry)) { 718 return; 719 } 720 721 // Add accept class if the domElement can accept the drag. 722 domElement.classList.add('accepts'); 723 this.destinationEntry_ = destinationEntry; 724 725 // Start timer changing the directory. 726 this.navigateTimer_ = setTimeout(function() { 727 if (domElement instanceof DirectoryItem) 728 // Do custom action. 729 (/** @type {DirectoryItem} */ domElement).doDropTargetAction(); 730 this.directoryModel_.changeDirectoryEntry(destinationEntry); 731 }.bind(this), 2000); 732 }, 733 734 /** 735 * Handles touch start. 736 */ 737 onTouchStart_: function() { 738 this.touching_ = true; 739 }, 740 741 /** 742 * Handles touch end. 743 */ 744 onTouchEnd_: function(event) { 745 // TODO(fukino): We have to check if event.touches.length be 0 to support 746 // multi-touch operations, but event.touches has incorrect value by a bug 747 // (crbug.com/373125). 748 // After the bug is fixed, we should check event.touches. 749 this.touching_ = false; 750 }, 751 752 /** 753 * Clears the drop target. 754 * @this {FileTransferController} 755 */ 756 clearDropTarget_: function() { 757 if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts')) 758 this.dropTarget_.classList.remove('accepts'); 759 this.dropTarget_ = null; 760 this.destinationEntry_ = null; 761 if (this.navigateTimer_ !== undefined) { 762 clearTimeout(this.navigateTimer_); 763 this.navigateTimer_ = undefined; 764 } 765 }, 766 767 /** 768 * @this {FileTransferController} 769 * @return {boolean} Returns false if {@code <input type="text">} element is 770 * currently active. Otherwise, returns true. 771 */ 772 isDocumentWideEvent_: function() { 773 return this.document_.activeElement.nodeName.toLowerCase() !== 'input' || 774 this.document_.activeElement.type.toLowerCase() !== 'text'; 775 }, 776 777 /** 778 * @this {FileTransferController} 779 */ 780 onCopy_: function(event) { 781 if (!this.isDocumentWideEvent_() || 782 !this.canCopyOrDrag_()) { 783 return; 784 } 785 event.preventDefault(); 786 this.cutOrCopy_(event.clipboardData, 'copy'); 787 this.notify_('selection-copied'); 788 }, 789 790 /** 791 * @this {FileTransferController} 792 */ 793 onBeforeCopy_: function(event) { 794 if (!this.isDocumentWideEvent_()) 795 return; 796 797 // queryCommandEnabled returns true if event.defaultPrevented is true. 798 if (this.canCopyOrDrag_()) 799 event.preventDefault(); 800 }, 801 802 /** 803 * @this {FileTransferController} 804 * @return {boolean} Returns true if all selected files are available to be 805 * copied. 806 */ 807 isAllSelectedFilesAvailable_: function() { 808 if (!this.currentDirectoryContentEntry) 809 return false; 810 var volumeInfo = this.volumeManager_.getVolumeInfo( 811 this.currentDirectoryContentEntry); 812 if (!volumeInfo) 813 return false; 814 var isDriveOffline = this.volumeManager_.getDriveConnectionState().type === 815 VolumeManagerCommon.DriveConnectionType.OFFLINE; 816 if (this.isOnDrive && isDriveOffline && !this.allDriveFilesAvailable) 817 return false; 818 return true; 819 }, 820 821 /** 822 * @this {FileTransferController} 823 * @return {boolean} Returns true if some files are selected and all the file 824 * on drive is available to be copied. Otherwise, returns false. 825 */ 826 canCopyOrDrag_: function() { 827 return this.isAllSelectedFilesAvailable_() && 828 this.selectedEntries_.length > 0; 829 }, 830 831 /** 832 * @this {FileTransferController} 833 */ 834 onCut_: function(event) { 835 if (!this.isDocumentWideEvent_() || 836 !this.canCutOrDrag_()) { 837 return; 838 } 839 event.preventDefault(); 840 this.cutOrCopy_(event.clipboardData, 'move'); 841 this.notify_('selection-cut'); 842 }, 843 844 /** 845 * @this {FileTransferController} 846 */ 847 onBeforeCut_: function(event) { 848 if (!this.isDocumentWideEvent_()) 849 return; 850 // queryCommandEnabled returns true if event.defaultPrevented is true. 851 if (this.canCutOrDrag_()) 852 event.preventDefault(); 853 }, 854 855 /** 856 * @this {FileTransferController} 857 * @return {boolean} Returns true if the current directory is not read only. 858 */ 859 canCutOrDrag_: function() { 860 return !this.readonly && this.selectedEntries_.length > 0; 861 }, 862 863 /** 864 * @this {FileTransferController} 865 */ 866 onPaste_: function(event) { 867 // If the event has destDirectory property, paste files into the directory. 868 // This occurs when the command fires from menu item 'Paste into folder'. 869 var destination = event.destDirectory || this.currentDirectoryContentEntry; 870 871 // Need to update here since 'beforepaste' doesn't fire. 872 if (!this.isDocumentWideEvent_() || 873 !this.canPasteOrDrop_(event.clipboardData, destination)) { 874 return; 875 } 876 event.preventDefault(); 877 var effect = this.paste(event.clipboardData, destination); 878 879 // On cut, we clear the clipboard after the file is pasted/moved so we don't 880 // try to move/delete the original file again. 881 if (effect === 'move') { 882 this.simulateCommand_('cut', function(event) { 883 event.preventDefault(); 884 event.clipboardData.setData('fs/clear', ''); 885 }); 886 } 887 }, 888 889 /** 890 * @this {FileTransferController} 891 */ 892 onBeforePaste_: function(event) { 893 if (!this.isDocumentWideEvent_()) 894 return; 895 // queryCommandEnabled returns true if event.defaultPrevented is true. 896 if (this.canPasteOrDrop_(event.clipboardData, 897 this.currentDirectoryContentEntry)) { 898 event.preventDefault(); 899 } 900 }, 901 902 /** 903 * @this {FileTransferController} 904 * @param {DataTransfer} dataTransfer Data transfer object. 905 * @param {DirectoryEntry} destinationEntry Destination entry. 906 * @return {boolean} Returns true if items stored in {@code dataTransfer} can 907 * be pasted to {@code destinationEntry}. Otherwise, returns false. 908 */ 909 canPasteOrDrop_: function(dataTransfer, destinationEntry) { 910 if (!destinationEntry) 911 return false; 912 var destinationLocationInfo = 913 this.volumeManager_.getLocationInfo(destinationEntry); 914 if (!destinationLocationInfo || destinationLocationInfo.isReadOnly) 915 return false; 916 if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') === -1) 917 return false; // Unsupported type of content. 918 919 // Copying between different sources requires all files to be available. 920 if (this.getSourceRootURL_(dataTransfer) !== 921 destinationLocationInfo.volumeInfo.fileSystem.root.toURL() && 922 this.isMissingFileContents_(dataTransfer)) 923 return false; 924 925 return true; 926 }, 927 928 /** 929 * Execute paste command. 930 * 931 * @this {FileTransferController} 932 * @return {boolean} Returns true, the paste is success. Otherwise, returns 933 * false. 934 */ 935 queryPasteCommandEnabled: function() { 936 if (!this.isDocumentWideEvent_()) { 937 return false; 938 } 939 940 // HACK(serya): return this.document_.queryCommandEnabled('paste') 941 // should be used. 942 var result; 943 this.simulateCommand_('paste', function(event) { 944 result = this.canPasteOrDrop_(event.clipboardData, 945 this.currentDirectoryContentEntry); 946 }.bind(this)); 947 return result; 948 }, 949 950 /** 951 * Allows to simulate commands to get access to clipboard. 952 * 953 * @this {FileTransferController} 954 * @param {string} command 'copy', 'cut' or 'paste'. 955 * @param {function} handler Event handler. 956 */ 957 simulateCommand_: function(command, handler) { 958 var iframe = this.document_.querySelector('#command-dispatcher'); 959 var doc = iframe.contentDocument; 960 doc.addEventListener(command, handler); 961 doc.execCommand(command); 962 doc.removeEventListener(command, handler); 963 }, 964 965 /** 966 * @this {FileTransferController} 967 */ 968 onSelectionChanged_: function(event) { 969 var entries = this.selectedEntries_; 970 var files = this.selectedFileObjects_ = []; 971 this.preloadedThumbnailImagePromise_ = null; 972 973 var fileEntries = []; 974 for (var i = 0; i < entries.length; i++) { 975 if (entries[i].isFile) 976 fileEntries.push(entries[i]); 977 } 978 var containsDirectory = fileEntries.length !== entries.length; 979 980 // File object must be prepeared in advance for clipboard operations 981 // (copy, paste and drag). DataTransfer object closes for write after 982 // returning control from that handlers so they may not have 983 // asynchronous operations. 984 if (!containsDirectory) { 985 for (var i = 0; i < fileEntries.length; i++) { 986 fileEntries[i].file(function(file) { files.push(file); }); 987 } 988 } 989 990 if (entries.length === 1) { 991 // For single selection, the dragged element is created in advance, 992 // otherwise an image may not be loaded at the time the 'dragstart' event 993 // comes. 994 this.preloadThumbnailImage_(entries[0]); 995 } 996 997 if (this.isOnDrive) { 998 this.allDriveFilesAvailable = false; 999 this.metadataCache_.get(entries, 'drive', function(props) { 1000 // We consider directories not available offline for the purposes of 1001 // file transfer since we cannot afford to recursive traversal. 1002 this.allDriveFilesAvailable = 1003 !containsDirectory && 1004 props.filter(function(p) { 1005 return !p.availableOffline; 1006 }).length === 0; 1007 // |Copy| is the only menu item affected by allDriveFilesAvailable. 1008 // It could be open right now, update its UI. 1009 this.copyCommand_.disabled = !this.canCopyOrDrag_(); 1010 }.bind(this)); 1011 } 1012 }, 1013 1014 /** 1015 * Obains directory that is displaying now. 1016 * @this {FileTransferController} 1017 * @return {DirectoryEntry} Entry of directry that is displaying now. 1018 */ 1019 get currentDirectoryContentEntry() { 1020 return this.directoryModel_.getCurrentDirEntry(); 1021 }, 1022 1023 /** 1024 * @this {FileTransferController} 1025 * @return {boolean} True if the current directory is read only. 1026 */ 1027 get readonly() { 1028 return this.directoryModel_.isReadOnly(); 1029 }, 1030 1031 /** 1032 * @this {FileTransferController} 1033 * @return {boolean} True if the current directory is on Drive. 1034 */ 1035 get isOnDrive() { 1036 var currentDir = this.directoryModel_.getCurrentDirEntry(); 1037 if (!currentDir) 1038 return false; 1039 var locationInfo = this.volumeManager_.getLocationInfo(currentDir); 1040 if (!locationInfo) 1041 return false; 1042 return locationInfo.isDriveBased; 1043 }, 1044 1045 /** 1046 * @this {FileTransferController} 1047 */ 1048 notify_: function(eventName) { 1049 var self = this; 1050 // Set timeout to avoid recursive events. 1051 setTimeout(function() { 1052 cr.dispatchSimpleEvent(self, eventName); 1053 }, 0); 1054 }, 1055 1056 /** 1057 * @this {FileTransferController} 1058 * @return {Array.<Entry>} Array of the selected entries. 1059 */ 1060 get selectedEntries_() { 1061 var list = this.directoryModel_.getFileList(); 1062 var selectedIndexes = this.directoryModel_.getFileListSelection(). 1063 selectedIndexes; 1064 var entries = selectedIndexes.map(function(index) { 1065 return list.item(index); 1066 }); 1067 1068 // TODO(serya): Diagnostics for http://crbug/129642 1069 if (entries.indexOf(undefined) !== -1) { 1070 var index = entries.indexOf(undefined); 1071 entries = entries.filter(function(e) { return !!e; }); 1072 console.error('Invalid selection found: list items: ', list.length, 1073 'wrong indexe value: ', selectedIndexes[index], 1074 'Stack trace: ', new Error().stack); 1075 } 1076 return entries; 1077 }, 1078 1079 /** 1080 * @param {Event} event Drag event. 1081 * @param {DirectoryEntry} destinationEntry Destination entry. 1082 * @this {FileTransferController} 1083 * @return {string} Returns the appropriate drop query type ('none', 'move' 1084 * or copy') to the current modifiers status and the destination. 1085 */ 1086 selectDropEffect_: function(event, destinationEntry) { 1087 if (!destinationEntry) 1088 return 'none'; 1089 var destinationLocationInfo = 1090 this.volumeManager_.getLocationInfo(destinationEntry); 1091 if (!destinationLocationInfo) 1092 return 'none'; 1093 if (destinationLocationInfo.isReadOnly) 1094 return 'none'; 1095 if (util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'move')) { 1096 if (!util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'copy')) 1097 return 'move'; 1098 // TODO(mtomasz): Use volumeId instead of comparing roots, as soon as 1099 // volumeId gets unique. 1100 if (this.getSourceRootURL_(event.dataTransfer) === 1101 destinationLocationInfo.volumeInfo.fileSystem.root.toURL() && 1102 !event.ctrlKey) { 1103 return 'move'; 1104 } 1105 if (event.shiftKey) { 1106 return 'move'; 1107 } 1108 } 1109 return 'copy'; 1110 }, 1111}; 1112