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 * @constructor 22 */ 23function FileTransferController(doc, 24 fileOperationManager, 25 metadataCache, 26 directoryModel) { 27 this.document_ = doc; 28 this.fileOperationManager_ = fileOperationManager; 29 this.metadataCache_ = metadataCache; 30 this.directoryModel_ = directoryModel; 31 32 this.directoryModel_.getFileListSelection().addEventListener('change', 33 this.onSelectionChanged_.bind(this)); 34 35 /** 36 * DOM element to represent selected file in drag operation. Used if only 37 * one element is selected. 38 * @type {HTMLElement} 39 * @private 40 */ 41 this.preloadedThumbnailImageNode_ = null; 42 43 /** 44 * File objects for selected files. 45 * 46 * @type {Array.<File>} 47 * @private 48 */ 49 this.selectedFileObjects_ = []; 50 51 /** 52 * Drag selector. 53 * @type {DragSelector} 54 * @private 55 */ 56 this.dragSelector_ = new DragSelector(); 57 58 /** 59 * Whether a user is touching the device or not. 60 * @type {boolean} 61 * @private 62 */ 63 this.touching_ = false; 64} 65 66FileTransferController.prototype = { 67 __proto__: cr.EventTarget.prototype, 68 69 /** 70 * @this {FileTransferController} 71 * @param {cr.ui.List} list Items in the list will be draggable. 72 */ 73 attachDragSource: function(list) { 74 list.style.webkitUserDrag = 'element'; 75 list.addEventListener('dragstart', this.onDragStart_.bind(this, list)); 76 list.addEventListener('dragend', this.onDragEnd_.bind(this, list)); 77 list.addEventListener('touchstart', this.onTouchStart_.bind(this)); 78 list.addEventListener('touchend', this.onTouchEnd_.bind(this)); 79 }, 80 81 /** 82 * @this {FileTransferController} 83 * @param {cr.ui.List} list List itself and its directory items will could 84 * be drop target. 85 * @param {boolean=} opt_onlyIntoDirectories If true only directory list 86 * items could be drop targets. Otherwise any other place of the list 87 * accetps files (putting it into the current directory). 88 */ 89 attachFileListDropTarget: function(list, opt_onlyIntoDirectories) { 90 list.addEventListener('dragover', this.onDragOver_.bind(this, 91 !!opt_onlyIntoDirectories, list)); 92 list.addEventListener('dragenter', 93 this.onDragEnterFileList_.bind(this, list)); 94 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); 95 list.addEventListener('drop', 96 this.onDrop_.bind(this, !!opt_onlyIntoDirectories)); 97 }, 98 99 /** 100 * @this {FileTransferController} 101 * @param {DirectoryTree} tree Its sub items will could be drop target. 102 */ 103 attachTreeDropTarget: function(tree) { 104 tree.addEventListener('dragover', this.onDragOver_.bind(this, true, tree)); 105 tree.addEventListener('dragenter', this.onDragEnterTree_.bind(this, tree)); 106 tree.addEventListener('dragleave', this.onDragLeave_.bind(this, tree)); 107 tree.addEventListener('drop', this.onDrop_.bind(this, true)); 108 }, 109 110 /** 111 * @this {FileTransferController} 112 * @param {NavigationList} tree Its sub items will could be drop target. 113 */ 114 attachNavigationListDropTarget: function(list) { 115 list.addEventListener('dragover', 116 this.onDragOver_.bind(this, true /* onlyIntoDirectories */, list)); 117 list.addEventListener('dragenter', 118 this.onDragEnterVolumesList_.bind(this, list)); 119 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); 120 list.addEventListener('drop', 121 this.onDrop_.bind(this, true /* onlyIntoDirectories */)); 122 }, 123 124 /** 125 * Attach handlers of copy, cut and paste operations to the document. 126 * 127 * @this {FileTransferController} 128 */ 129 attachCopyPasteHandlers: function() { 130 this.document_.addEventListener('beforecopy', 131 this.onBeforeCopy_.bind(this)); 132 this.document_.addEventListener('copy', 133 this.onCopy_.bind(this)); 134 this.document_.addEventListener('beforecut', 135 this.onBeforeCut_.bind(this)); 136 this.document_.addEventListener('cut', 137 this.onCut_.bind(this)); 138 this.document_.addEventListener('beforepaste', 139 this.onBeforePaste_.bind(this)); 140 this.document_.addEventListener('paste', 141 this.onPaste_.bind(this)); 142 this.copyCommand_ = this.document_.querySelector('command#copy'); 143 }, 144 145 /** 146 * Write the current selection to system clipboard. 147 * 148 * @this {FileTransferController} 149 * @param {DataTransfer} dataTransfer DataTransfer from the event. 150 * @param {string} effectAllowed Value must be valid for the 151 * |dataTransfer.effectAllowed| property ('move', 'copy', 'copyMove'). 152 */ 153 cutOrCopy_: function(dataTransfer, effectAllowed) { 154 // Tag to check it's filemanager data. 155 dataTransfer.setData('fs/tag', 'filemanager-data'); 156 dataTransfer.setData('fs/sourceRoot', 157 this.directoryModel_.getCurrentRootPath()); 158 var sourcePaths = 159 this.selectedEntries_.map(function(e) { return e.fullPath; }); 160 dataTransfer.setData('fs/sources', sourcePaths.join('\n')); 161 dataTransfer.effectAllowed = effectAllowed; 162 dataTransfer.setData('fs/effectallowed', effectAllowed); 163 164 for (var i = 0; i < this.selectedFileObjects_.length; i++) { 165 dataTransfer.items.add(this.selectedFileObjects_[i]); 166 } 167 }, 168 169 /** 170 * Extracts source root from the |dataTransfer| object. 171 * 172 * @this {FileTransferController} 173 * @param {DataTransfer} dataTransfer DataTransfer object from the event. 174 * @return {string} Path or empty string (if unknown). 175 */ 176 getSourceRoot_: function(dataTransfer) { 177 var sourceRoot = dataTransfer.getData('fs/sourceRoot'); 178 if (sourceRoot) 179 return sourceRoot; 180 181 // |dataTransfer| in protected mode. 182 if (window[DRAG_AND_DROP_GLOBAL_DATA]) 183 return window[DRAG_AND_DROP_GLOBAL_DATA].sourceRoot; 184 185 // Dragging from other tabs/windows. 186 var views = chrome && chrome.extension ? chrome.extension.getViews() : []; 187 for (var i = 0; i < views.length; i++) { 188 if (views[i][DRAG_AND_DROP_GLOBAL_DATA]) 189 return views[i][DRAG_AND_DROP_GLOBAL_DATA].sourceRoot; 190 } 191 192 // Unknown source. 193 return ''; 194 }, 195 196 /** 197 * Queue up a file copy operation based on the current system clipboard. 198 * 199 * @this {FileTransferController} 200 * @param {DataTransfer} dataTransfer System data transfer object. 201 * @param {string=} opt_destinationPath Paste destination. 202 * @param {string=} opt_effect Desired drop/paste effect. Could be 203 * 'move'|'copy' (default is copy). Ignored if conflicts with 204 * |dataTransfer.effectAllowed|. 205 * @return {string} Either "copy" or "move". 206 */ 207 paste: function(dataTransfer, opt_destinationPath, opt_effect) { 208 var sourcePaths = (dataTransfer.getData('fs/sources') || '').split('\n'); 209 var destinationPath = opt_destinationPath || 210 this.currentDirectoryContentPath; 211 // effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers 212 // work fine. 213 var effectAllowed = dataTransfer.effectAllowed != 'uninitialized' ? 214 dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed'); 215 var toMove = effectAllowed == 'move' || 216 (effectAllowed == 'copyMove' && opt_effect == 'move'); 217 218 // Start the pasting operation. 219 this.fileOperationManager_.paste(sourcePaths, destinationPath, toMove); 220 return toMove ? 'move' : 'copy'; 221 }, 222 223 /** 224 * Preloads an image thumbnail for the specified file entry. 225 * 226 * @this {FileTransferController} 227 * @param {Entry} entry Entry to preload a thumbnail for. 228 */ 229 preloadThumbnailImage_: function(entry) { 230 var metadataTypes = 'thumbnail|filesystem'; 231 var thumbnailContainer = this.document_.createElement('div'); 232 this.preloadedThumbnailImageNode_ = thumbnailContainer; 233 this.preloadedThumbnailImageNode_.className = 'img-container'; 234 this.metadataCache_.get( 235 entry, 236 metadataTypes, 237 function(metadata) { 238 new ThumbnailLoader(entry.toURL(), 239 ThumbnailLoader.LoaderType.IMAGE, 240 metadata). 241 load(thumbnailContainer, 242 ThumbnailLoader.FillMode.FILL); 243 }.bind(this)); 244 }, 245 246 /** 247 * Renders a drag-and-drop thumbnail. 248 * 249 * @this {FileTransferController} 250 * @return {HTMLElement} Element containing the thumbnail. 251 */ 252 renderThumbnail_: function() { 253 var length = this.selectedEntries_.length; 254 255 var container = this.document_.querySelector('#drag-container'); 256 var contents = this.document_.createElement('div'); 257 contents.className = 'drag-contents'; 258 container.appendChild(contents); 259 260 var thumbnailImage; 261 if (this.preloadedThumbnailImageNode_) 262 thumbnailImage = this.preloadedThumbnailImageNode_.querySelector('img'); 263 264 // Option 1. Multiple selection, render only a label. 265 if (length > 1) { 266 var label = this.document_.createElement('div'); 267 label.className = 'label'; 268 label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length); 269 contents.appendChild(label); 270 return container; 271 } 272 273 // Option 2. Thumbnail image available, then render it without 274 // a label. 275 if (thumbnailImage) { 276 thumbnailImage.classList.add('drag-thumbnail'); 277 contents.classList.add('for-image'); 278 contents.appendChild(this.preloadedThumbnailImageNode_); 279 return container; 280 } 281 282 // Option 3. Thumbnail not available. Render an icon and a label. 283 var entry = this.selectedEntries_[0]; 284 var icon = this.document_.createElement('div'); 285 icon.className = 'detail-icon'; 286 icon.setAttribute('file-type-icon', FileType.getIcon(entry)); 287 contents.appendChild(icon); 288 var label = this.document_.createElement('div'); 289 label.className = 'label'; 290 label.textContent = entry.name; 291 contents.appendChild(label); 292 return container; 293 }, 294 295 /** 296 * @this {FileTransferController} 297 * @param {cr.ui.List} list Drop target list 298 * @param {Event} event A dragstart event of DOM. 299 */ 300 onDragStart_: function(list, event) { 301 // If a user is touching, Files.app does not receive drag operations. 302 if (this.touching_) { 303 event.preventDefault(); 304 return; 305 } 306 307 // Check if a drag selection should be initiated or not. 308 if (list.shouldStartDragSelection(event)) { 309 this.dragSelector_.startDragSelection(list, event); 310 return; 311 } 312 313 // Nothing selected. 314 if (!this.selectedEntries_.length) { 315 event.preventDefault(); 316 return; 317 } 318 319 var dt = event.dataTransfer; 320 321 if (this.canCopyOrDrag_(dt)) { 322 if (this.canCutOrDrag_(dt)) 323 this.cutOrCopy_(dt, 'copyMove'); 324 else 325 this.cutOrCopy_(dt, 'copy'); 326 } else { 327 event.preventDefault(); 328 return; 329 } 330 331 var dragThumbnail = this.renderThumbnail_(); 332 dt.setDragImage(dragThumbnail, 1000, 1000); 333 334 window[DRAG_AND_DROP_GLOBAL_DATA] = { 335 sourceRoot: this.directoryModel_.getCurrentRootPath() 336 }; 337 }, 338 339 /** 340 * @this {FileTransferController} 341 * @param {cr.ui.List} list Drop target list. 342 * @param {Event} event A dragend event of DOM. 343 */ 344 onDragEnd_: function(list, event) { 345 var container = this.document_.querySelector('#drag-container'); 346 container.textContent = ''; 347 this.clearDropTarget_(); 348 delete window[DRAG_AND_DROP_GLOBAL_DATA]; 349 }, 350 351 /** 352 * @this {FileTransferController} 353 * @param {boolean} onlyIntoDirectories True if the drag is only into 354 * directories. 355 * @param {cr.ui.List} list Drop target list. 356 * @param {Event} event A dragover event of DOM. 357 */ 358 onDragOver_: function(onlyIntoDirectories, list, event) { 359 event.preventDefault(); 360 var path = this.destinationPath_ || 361 (!onlyIntoDirectories && this.currentDirectoryContentPath); 362 event.dataTransfer.dropEffect = this.selectDropEffect_(event, path); 363 event.preventDefault(); 364 }, 365 366 /** 367 * @this {FileTransferController} 368 * @param {cr.ui.List} list Drop target list. 369 * @param {Event} event A dragenter event of DOM. 370 */ 371 onDragEnterFileList_: function(list, event) { 372 event.preventDefault(); // Required to prevent the cursor flicker. 373 this.lastEnteredTarget_ = event.target; 374 var item = list.getListItemAncestor(event.target); 375 item = item && list.isItem(item) ? item : null; 376 if (item == this.dropTarget_) 377 return; 378 379 var entry = item && list.dataModel.item(item.listIndex); 380 if (entry) { 381 this.setDropTarget_(item, entry.isDirectory, event.dataTransfer, 382 entry.fullPath); 383 } else { 384 this.clearDropTarget_(); 385 } 386 }, 387 388 /** 389 * @this {FileTransferController} 390 * @param {DirectoryTree} tree Drop target tree. 391 * @param {Event} event A dragenter event of DOM. 392 */ 393 onDragEnterTree_: function(tree, event) { 394 event.preventDefault(); // Required to prevent the cursor flicker. 395 this.lastEnteredTarget_ = event.target; 396 var item = event.target; 397 while (item && !(item instanceof DirectoryItem)) { 398 item = item.parentNode; 399 } 400 401 if (item == this.dropTarget_) 402 return; 403 404 var entry = item && item.entry; 405 if (entry) { 406 this.setDropTarget_(item, entry.isDirectory, event.dataTransfer, 407 entry.fullPath); 408 } else { 409 this.clearDropTarget_(); 410 } 411 }, 412 413 /** 414 * @this {FileTransferController} 415 * @param {NavigationList} list Drop target list. 416 * @param {Event} event A dragenter event of DOM. 417 */ 418 onDragEnterVolumesList_: function(list, event) { 419 event.preventDefault(); // Required to prevent the cursor flicker. 420 this.lastEnteredTarget_ = event.target; 421 var item = list.getListItemAncestor(event.target); 422 item = item && list.isItem(item) ? item : null; 423 if (item == this.dropTarget_) 424 return; 425 426 var path = item && list.dataModel.item(item.listIndex).path; 427 if (path) 428 this.setDropTarget_(item, true /* directory */, event.dataTransfer, path); 429 else 430 this.clearDropTarget_(); 431 }, 432 433 /** 434 * @this {FileTransferController} 435 * @param {cr.ui.List} list Drop target list. 436 * @param {Event} event A dragleave event of DOM. 437 */ 438 onDragLeave_: function(list, event) { 439 // If mouse moves from one element to another the 'dragenter' 440 // event for the new element comes before the 'dragleave' event for 441 // the old one. In this case event.target != this.lastEnteredTarget_ 442 // and handler of the 'dragenter' event has already caried of 443 // drop target. So event.target == this.lastEnteredTarget_ 444 // could only be if mouse goes out of listened element. 445 if (event.target == this.lastEnteredTarget_) { 446 this.clearDropTarget_(); 447 this.lastEnteredTarget_ = null; 448 } 449 }, 450 451 /** 452 * @this {FileTransferController} 453 * @param {boolean} onlyIntoDirectories True if the drag is only into 454 * directories. 455 * @param {Event} event A dragleave event of DOM. 456 */ 457 onDrop_: function(onlyIntoDirectories, event) { 458 if (onlyIntoDirectories && !this.dropTarget_) 459 return; 460 var destinationPath = this.destinationPath_ || 461 this.currentDirectoryContentPath; 462 if (!this.canPasteOrDrop_(event.dataTransfer, destinationPath)) 463 return; 464 event.preventDefault(); 465 this.paste(event.dataTransfer, destinationPath, 466 this.selectDropEffect_(event, destinationPath)); 467 this.clearDropTarget_(); 468 }, 469 470 /** 471 * Sets the drop target. 472 * @this {FileTransferController} 473 * @param {Element} domElement Target of the drop. 474 * @param {boolean} isDirectory If the target is a directory. 475 * @param {DataTransfer} dataTransfer Data transfer object. 476 * @param {string} destinationPath Destination path. 477 */ 478 setDropTarget_: function(domElement, isDirectory, dataTransfer, 479 destinationPath) { 480 if (this.dropTarget_ == domElement) 481 return; 482 483 // Remove the old drop target. 484 this.clearDropTarget_(); 485 486 // Set the new drop target. 487 this.dropTarget_ = domElement; 488 489 if (!domElement || 490 !isDirectory || 491 !this.canPasteOrDrop_(dataTransfer, destinationPath)) { 492 return; 493 } 494 495 // Add accept class if the domElement can accept the drag. 496 domElement.classList.add('accepts'); 497 this.destinationPath_ = destinationPath; 498 499 // Start timer changing the directory. 500 this.navigateTimer_ = setTimeout(function() { 501 if (domElement instanceof DirectoryItem) 502 // Do custom action. 503 (/** @type {DirectoryItem} */ domElement).doDropTargetAction(); 504 this.directoryModel_.changeDirectory(destinationPath); 505 }.bind(this), 2000); 506 }, 507 508 /** 509 * Handles touch start. 510 */ 511 onTouchStart_: function() { 512 this.touching_ = true; 513 }, 514 515 /** 516 * Handles touch end. 517 */ 518 onTouchEnd_: function(event) { 519 if (event.touches.length === 0) 520 this.touching_ = false; 521 }, 522 523 /** 524 * Clears the drop target. 525 * @this {FileTransferController} 526 */ 527 clearDropTarget_: function() { 528 if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts')) 529 this.dropTarget_.classList.remove('accepts'); 530 this.dropTarget_ = null; 531 this.destinationPath_ = null; 532 if (this.navigateTimer_ !== undefined) { 533 clearTimeout(this.navigateTimer_); 534 this.navigateTimer_ = undefined; 535 } 536 }, 537 538 /** 539 * @this {FileTransferController} 540 * @return {boolean} Returns false if {@code <input type="text">} element is 541 * currently active. Otherwise, returns true. 542 */ 543 isDocumentWideEvent_: function() { 544 return this.document_.activeElement.nodeName.toLowerCase() != 'input' || 545 this.document_.activeElement.type.toLowerCase() != 'text'; 546 }, 547 548 /** 549 * @this {FileTransferController} 550 */ 551 onCopy_: function(event) { 552 if (!this.isDocumentWideEvent_() || 553 !this.canCopyOrDrag_()) { 554 return; 555 } 556 event.preventDefault(); 557 this.cutOrCopy_(event.clipboardData, 'copy'); 558 this.notify_('selection-copied'); 559 }, 560 561 /** 562 * @this {FileTransferController} 563 */ 564 onBeforeCopy_: function(event) { 565 if (!this.isDocumentWideEvent_()) 566 return; 567 568 // queryCommandEnabled returns true if event.defaultPrevented is true. 569 if (this.canCopyOrDrag_()) 570 event.preventDefault(); 571 }, 572 573 /** 574 * @this {FileTransferController} 575 * @return {boolean} Returns true if some files are selected and all the file 576 * on drive is available to be copied. Otherwise, returns false. 577 */ 578 canCopyOrDrag_: function() { 579 if (this.isOnDrive && 580 this.directoryModel_.isDriveOffline() && 581 !this.allDriveFilesAvailable) 582 return false; 583 return this.selectedEntries_.length > 0; 584 }, 585 586 /** 587 * @this {FileTransferController} 588 */ 589 onCut_: function(event) { 590 if (!this.isDocumentWideEvent_() || 591 !this.canCutOrDrag_()) { 592 return; 593 } 594 event.preventDefault(); 595 this.cutOrCopy_(event.clipboardData, 'move'); 596 this.notify_('selection-cut'); 597 }, 598 599 /** 600 * @this {FileTransferController} 601 */ 602 onBeforeCut_: function(event) { 603 if (!this.isDocumentWideEvent_()) 604 return; 605 // queryCommandEnabled returns true if event.defaultPrevented is true. 606 if (this.canCutOrDrag_()) 607 event.preventDefault(); 608 }, 609 610 /** 611 * @this {FileTransferController} 612 * @return {boolean} Returns true if some files are selected and all the file 613 * on drive is available to be cut. Otherwise, returns false. 614 */ 615 canCutOrDrag_: function() { 616 return !this.readonly && this.canCopyOrDrag_(); 617 }, 618 619 /** 620 * @this {FileTransferController} 621 */ 622 onPaste_: function(event) { 623 // Need to update here since 'beforepaste' doesn't fire. 624 if (!this.isDocumentWideEvent_() || 625 !this.canPasteOrDrop_(event.clipboardData, 626 this.currentDirectoryContentPath)) { 627 return; 628 } 629 event.preventDefault(); 630 var effect = this.paste(event.clipboardData); 631 632 // On cut, we clear the clipboard after the file is pasted/moved so we don't 633 // try to move/delete the original file again. 634 if (effect == 'move') { 635 this.simulateCommand_('cut', function(event) { 636 event.preventDefault(); 637 event.clipboardData.setData('fs/clear', ''); 638 }); 639 } 640 }, 641 642 /** 643 * @this {FileTransferController} 644 */ 645 onBeforePaste_: function(event) { 646 if (!this.isDocumentWideEvent_()) 647 return; 648 // queryCommandEnabled returns true if event.defaultPrevented is true. 649 if (this.canPasteOrDrop_(event.clipboardData, 650 this.currentDirectoryContentPath)) { 651 event.preventDefault(); 652 } 653 }, 654 655 /** 656 * @this {FileTransferController} 657 * @param {DataTransfer} dataTransfer Data transfer object. 658 * @param {string?} destinationPath Destination path. 659 * @return {boolean} Returns true if items stored in {@code dataTransfer} can 660 * be pasted to {@code destinationPath}. Otherwise, returns false. 661 */ 662 canPasteOrDrop_: function(dataTransfer, destinationPath) { 663 if (!destinationPath) { 664 return false; 665 } 666 if (this.directoryModel_.isPathReadOnly(destinationPath)) { 667 return false; 668 } 669 if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') == -1) { 670 return false; // Unsupported type of content. 671 } 672 if (dataTransfer.getData('fs/tag') == '') { 673 // Data protected. Other checks are not possible but it makes sense to 674 // let the user try. 675 return true; 676 } 677 678 var directories = dataTransfer.getData('fs/directories').split('\n'). 679 filter(function(d) { return d != ''; }); 680 681 for (var i = 0; i < directories.length; i++) { 682 if (destinationPath.substr(0, directories[i].length) == directories[i]) 683 return false; // recursive paste. 684 } 685 686 return true; 687 }, 688 689 /** 690 * Execute paste command. 691 * 692 * @this {FileTransferController} 693 * @return {boolean} Returns true, the paste is success. Otherwise, returns 694 * false. 695 */ 696 queryPasteCommandEnabled: function() { 697 if (!this.isDocumentWideEvent_()) { 698 return false; 699 } 700 701 // HACK(serya): return this.document_.queryCommandEnabled('paste') 702 // should be used. 703 var result; 704 this.simulateCommand_('paste', function(event) { 705 result = this.canPasteOrDrop_(event.clipboardData, 706 this.currentDirectoryContentPath); 707 }.bind(this)); 708 return result; 709 }, 710 711 /** 712 * Allows to simulate commands to get access to clipboard. 713 * 714 * @this {FileTransferController} 715 * @param {string} command 'copy', 'cut' or 'paste'. 716 * @param {function} handler Event handler. 717 */ 718 simulateCommand_: function(command, handler) { 719 var iframe = this.document_.querySelector('#command-dispatcher'); 720 var doc = iframe.contentDocument; 721 doc.addEventListener(command, handler); 722 doc.execCommand(command); 723 doc.removeEventListener(command, handler); 724 }, 725 726 /** 727 * @this {FileTransferController} 728 */ 729 onSelectionChanged_: function(event) { 730 var entries = this.selectedEntries_; 731 var files = this.selectedFileObjects_ = []; 732 this.preloadedThumbnailImageNode_ = null; 733 734 var fileEntries = []; 735 for (var i = 0; i < entries.length; i++) { 736 if (entries[i].isFile) 737 fileEntries.push(entries[i]); 738 } 739 740 if (entries.length == 1) { 741 // For single selection, the dragged element is created in advance, 742 // otherwise an image may not be loaded at the time the 'dragstart' event 743 // comes. 744 this.preloadThumbnailImage_(entries[0]); 745 } 746 747 // File object must be prepeared in advance for clipboard operations 748 // (copy, paste and drag). DataTransfer object closes for write after 749 // returning control from that handlers so they may not have 750 // asynchronous operations. 751 var prepareFileObjects = function() { 752 for (var i = 0; i < fileEntries.length; i++) { 753 fileEntries[i].file(function(file) { files.push(file); }); 754 } 755 }; 756 757 if (this.isOnDrive) { 758 this.allDriveFilesAvailable = false; 759 this.metadataCache_.get( 760 entries, 'drive', function(props) { 761 // We consider directories not available offline for the purposes of 762 // file transfer since we cannot afford to recursive traversal. 763 this.allDriveFilesAvailable = 764 entries.filter(function(e) {return e.isDirectory}).length == 0 && 765 props.filter(function(p) {return !p.availableOffline}).length == 0; 766 // |Copy| is the only menu item affected by allDriveFilesAvailable. 767 // It could be open right now, update its UI. 768 this.copyCommand_.disabled = !this.canCopyOrDrag_(); 769 770 if (this.allDriveFilesAvailable) 771 prepareFileObjects(); 772 }.bind(this)); 773 } else { 774 prepareFileObjects(); 775 } 776 }, 777 778 /** 779 * Path of directory that is displaying now. 780 * If search result is displaying now, this is null. 781 * @this {FileTransferController} 782 * @return {string} Path of directry that is displaying now. 783 */ 784 get currentDirectoryContentPath() { 785 return this.directoryModel_.isSearching() ? 786 null : this.directoryModel_.getCurrentDirPath(); 787 }, 788 789 /** 790 * @this {FileTransferController} 791 * @return {boolean} True if the current directory is read only. 792 */ 793 get readonly() { 794 return this.directoryModel_.isReadOnly(); 795 }, 796 797 /** 798 * @this {FileTransferController} 799 * @return {boolean} True if the current directory is on Drive. 800 */ 801 get isOnDrive() { 802 return PathUtil.isDriveBasedPath(this.directoryModel_.getCurrentRootPath()); 803 }, 804 805 /** 806 * @this {FileTransferController} 807 */ 808 notify_: function(eventName) { 809 var self = this; 810 // Set timeout to avoid recursive events. 811 setTimeout(function() { 812 cr.dispatchSimpleEvent(self, eventName); 813 }, 0); 814 }, 815 816 /** 817 * @this {FileTransferController} 818 * @return {Array.<Entry>} Array of the selected entries. 819 */ 820 get selectedEntries_() { 821 var list = this.directoryModel_.getFileList(); 822 var selectedIndexes = this.directoryModel_.getFileListSelection(). 823 selectedIndexes; 824 var entries = selectedIndexes.map(function(index) { 825 return list.item(index); 826 }); 827 828 // TODO(serya): Diagnostics for http://crbug/129642 829 if (entries.indexOf(undefined) != -1) { 830 var index = entries.indexOf(undefined); 831 entries = entries.filter(function(e) { return !!e; }); 832 console.error('Invalid selection found: list items: ', list.length, 833 'wrong indexe value: ', selectedIndexes[index], 834 'Stack trace: ', new Error().stack); 835 } 836 return entries; 837 }, 838 839 /** 840 * @this {FileTransferController} 841 * @return {string} Returns the appropriate drop query type ('none', 'move' 842 * or copy') to the current modifiers status and the destination. 843 */ 844 selectDropEffect_: function(event, destinationPath) { 845 if (!destinationPath || 846 this.directoryModel_.isPathReadOnly(destinationPath)) 847 return 'none'; 848 if (event.dataTransfer.effectAllowed == 'copyMove' && 849 this.getSourceRoot_(event.dataTransfer) == 850 PathUtil.getRootPath(destinationPath) && 851 !event.ctrlKey) { 852 return 'move'; 853 } 854 if (event.dataTransfer.effectAllowed == 'copyMove' && 855 event.shiftKey) { 856 return 'move'; 857 } 858 return 'copy'; 859 }, 860}; 861