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