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.
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.
13 var DRAG_AND_DROP_GLOBAL_DATA = '__drag_and_drop_global_data';
16 * @param {HTMLDocument} doc Owning document.
17 * @param {FileOperationManager} fileOperationManager File operation manager
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.
26 function FileTransferController(doc,
31 multiProfileShareDialog) {
33 this.fileOperationManager_ = fileOperationManager;
34 this.metadataCache_ = metadataCache;
35 this.directoryModel_ = directoryModel;
36 this.volumeManager_ = volumeManager;
37 this.multiProfileShareDialog_ = multiProfileShareDialog;
39 this.directoryModel_.getFileList().addEventListener(
40 'change', function(event) {
41 if (this.directoryModel_.getFileListSelection().
42 getIndexSelected(event.index)) {
43 this.onSelectionChanged_();
46 this.directoryModel_.getFileListSelection().addEventListener('change',
47 this.onSelectionChanged_.bind(this));
50 * Promise to be fulfilled with the thumbnail image of selected file in drag
51 * operation. Used if only one element is selected.
55 this.preloadedThumbnailImagePromise_ = null;
58 * File objects for selected files.
60 * @type {Array.<File>}
63 this.selectedFileObjects_ = [];
67 * @type {DragSelector}
70 this.dragSelector_ = new DragSelector();
73 * Whether a user is touching the device or not.
77 this.touching_ = false;
81 * Size of drag thumbnail for image files.
87 FileTransferController.DRAG_THUMBNAIL_SIZE_ = 64;
89 FileTransferController.prototype = {
90 __proto__: cr.EventTarget.prototype,
93 * @this {FileTransferController}
94 * @param {cr.ui.List} list Items in the list will be draggable.
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);
108 * @this {FileTransferController}
109 * @param {cr.ui.List} list List itself and its directory items will could
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).
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));
126 * @this {FileTransferController}
127 * @param {DirectoryTree} tree Its sub items will could be drop target.
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));
137 * @this {FileTransferController}
138 * @param {NavigationList} tree Its sub items will could be drop target.
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 */));
151 * Attach handlers of copy, cut and paste operations to the document.
153 * @this {FileTransferController}
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');
172 * Write the current selection to system clipboard.
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.
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_());
194 for (var i = 0; i < this.selectedFileObjects_.length; i++) {
195 dataTransfer.items.add(this.selectedFileObjects_[i]);
200 * @this {FileTransferController}
201 * @return {Object.<string, string>} Drag and drop global data object.
203 getDragAndDropGlobalData_: function() {
204 if (window[DRAG_AND_DROP_GLOBAL_DATA])
205 return window[DRAG_AND_DROP_GLOBAL_DATA];
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];
217 * Extracts source root URL from the |dataTransfer| object.
219 * @this {FileTransferController}
220 * @param {DataTransfer} dataTransfer DataTransfer object from the event.
221 * @return {string} URL or an empty string (if unknown).
223 getSourceRootURL_: function(dataTransfer) {
224 var sourceRootURL = dataTransfer.getData('fs/sourceRootURL');
226 return sourceRootURL;
228 // |dataTransfer| in protected mode.
229 var globalData = this.getDragAndDropGlobalData_();
231 return globalData.sourceRootURL;
238 * @this {FileTransferController}
239 * @param {DataTransfer} dataTransfer DataTransfer object from the event.
240 * @return {boolean} Returns true when missing some file contents.
242 isMissingFileContents_: function(dataTransfer) {
243 var data = dataTransfer.getData('fs/missingFileContents');
245 // |dataTransfer| in protected mode.
246 var globalData = this.getDragAndDropGlobalData_();
248 data = globalData.missingFileContents;
250 return data === 'true';
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
260 getMultiProfileShareEntries_: function(entries) {
261 // Utility function to concat arrays.
262 var concatArrays = function(arrays) {
263 return Array.prototype.concat.apply([], arrays);
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);
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);
282 then(function(metadatas) {
283 return entries.filter(function(entry, i) {
284 var metadata = metadatas[i];
285 return metadata && metadata.isHosted && !metadata.sharedWithMe;
290 // Check child entries.
291 var processDirectoryEntry = function(entry) {
292 return readEntries(entry.createReader());
295 // Read entries from DirectoryReader and call processEntries for the chunk
297 var readEntries = function(reader) {
298 return new Promise(reader.readEntries.bind(reader)).then(
300 if (entries.length > 0) {
302 [processEntries(entries), readEntries(reader)]).
310 'Error happens while reading directory.', error);
315 // Filter entries that is owned by the current user, and call
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);
324 * Queue up a file copy operation based on the current system clipboard.
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".
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
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;
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);
356 then(function(shareEntries) {
357 if (shareEntries.length === 0)
359 return this.multiProfileShareDialog_.show(shareEntries.length > 1).
360 then(function(dialogResult) {
361 if (dialogResult === 'cancel')
362 return Promise.reject('ABORT');
364 // TODO(hirono): Make the loop cancellable.
365 var requestDriveShare = function(index) {
366 if (index >= shareEntries.length)
368 return new Promise(function(fulfill) {
369 chrome.fileBrowserPrivate.requestDriveShare(
370 shareEntries[index].toURL(),
373 // TODO(hirono): Check chrome.runtime.lastError here.
376 }).then(requestDriveShare.bind(null, index + 1));
378 return requestDriveShare(0);
382 // Start the pasting operation.
383 this.fileOperationManager_.paste(
384 entries, destinationEntry, toMove);
386 // Publish events for failureUrls.
387 for (var i = 0; i < failureUrls.length; i++) {
389 decodeURIComponent(failureUrls[i].replace(/^.+\//, ''));
390 var event = new Event('source-not-found');
391 event.fileName = fileName;
393 toMove ? ProgressItemType.MOVE : ProgressItemType.COPY;
394 this.dispatchEvent(event);
397 catch(function(error) {
398 if (error !== 'ABORT')
399 console.error(error.stack ? error.stack : error);
401 return toMove ? 'move' : 'copy';
405 * Preloads an image thumbnail for the specified file entry.
407 * @this {FileTransferController}
408 * @param {Entry} entry Entry to preload a thumbnail for.
410 preloadThumbnailImage_: function(entry) {
411 var metadataPromise = new Promise(function(fulfill, reject) {
412 this.metadataCache_.getOne(entry,
413 'thumbnail|filesystem',
418 reject('Failed to fetch metadata.');
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) {
428 fulfill(loader.getImage());
433 imagePromise.then(function(image) {
434 // Store the image so that we can obtain the image synchronously.
435 imagePromise.value = image;
437 console.error(error.stack || error);
440 this.preloadedThumbnailImagePromise_ = imagePromise;
444 * Renders a drag-and-drop thumbnail.
446 * @this {FileTransferController}
447 * @return {HTMLElement} Element containing the thumbnail.
449 renderThumbnail_: function() {
450 var length = this.selectedEntries_.length;
452 var container = this.document_.querySelector('#drag-container');
453 var contents = this.document_.createElement('div');
454 contents.className = 'drag-contents';
455 container.appendChild(contents);
457 // Option 1. Multiple selection, render only a label.
459 var label = this.document_.createElement('div');
460 label.className = 'label';
461 label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
462 contents.appendChild(label);
466 // Option 2. Thumbnail image available, then render it without
468 if (this.preloadedThumbnailImagePromise_ &&
469 this.preloadedThumbnailImagePromise_.value) {
470 var thumbnailImage = this.preloadedThumbnailImagePromise_.value;
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_;
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);
483 var context = canvas.getContext('2d');
484 context.drawImage(thumbnailImage,
485 (thumbnailImage.width - srcWidth) / 2,
486 (thumbnailImage.height - srcHeight) / 2,
493 contents.classList.add('for-image');
494 contents.appendChild(canvas);
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);
512 * @this {FileTransferController}
513 * @param {cr.ui.List} list Drop target list
514 * @param {Event} event A dragstart event of DOM.
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.
522 this.dragSelector_.startDragSelection(list, event);
527 if (!this.selectedEntries_.length) {
528 event.preventDefault();
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');
541 this.cutOrCopy_(dt, 'move');
544 event.preventDefault();
548 var dragThumbnail = this.renderThumbnail_();
549 dt.setDragImage(dragThumbnail, 0, 0);
551 window[DRAG_AND_DROP_GLOBAL_DATA] = {
552 sourceRootURL: dt.getData('fs/sourceRootURL'),
553 missingFileContents: dt.getData('fs/missingFileContents'),
558 * @this {FileTransferController}
559 * @param {cr.ui.List} list Drop target list.
560 * @param {Event} event A dragend event of DOM.
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;
567 var container = this.document_.querySelector('#drag-container');
568 container.textContent = '';
569 this.clearDropTarget_();
570 delete window[DRAG_AND_DROP_GLOBAL_DATA];
574 * @this {FileTransferController}
575 * @param {boolean} onlyIntoDirectories True if the drag is only into
577 * @param {cr.ui.List} list Drop target list.
578 * @param {Event} event A dragover event of DOM.
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();
589 * @this {FileTransferController}
590 * @param {cr.ui.List} list Drop target list.
591 * @param {Event} event A dragenter event of DOM.
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_)
601 var entry = item && list.dataModel.item(item.listIndex);
603 this.setDropTarget_(item, event.dataTransfer, entry);
605 this.clearDropTarget_();
609 * @this {FileTransferController}
610 * @param {DirectoryTree} tree Drop target tree.
611 * @param {Event} event A dragenter event of DOM.
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 cr.ui.TreeItem)) {
618 item = item.parentNode;
621 if (item === this.dropTarget_)
624 var entry = item && item.entry;
626 this.setDropTarget_(item, event.dataTransfer, entry);
628 this.clearDropTarget_();
633 * @this {FileTransferController}
634 * @param {NavigationList} list Drop target list.
635 * @param {Event} event A dragenter event of DOM.
637 onDragEnterVolumesList_: function(list, event) {
638 event.preventDefault(); // Required to prevent the cursor flicker.
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_)
646 var modelItem = item && list.dataModel.item(item.listIndex);
647 if (modelItem && modelItem.isShortcut) {
648 this.setDropTarget_(item, event.dataTransfer, modelItem.entry);
651 if (modelItem && modelItem.isVolume && modelItem.volumeInfo.displayRoot) {
653 item, event.dataTransfer, modelItem.volumeInfo.displayRoot);
657 this.clearDropTarget_();
661 * @this {FileTransferController}
662 * @param {cr.ui.List} list Drop target list.
663 * @param {Event} event A dragleave event of DOM.
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;
679 * @this {FileTransferController}
680 * @param {boolean} onlyIntoDirectories True if the drag is only into
682 * @param {Event} event A dragleave event of DOM.
684 onDrop_: function(onlyIntoDirectories, event) {
685 if (onlyIntoDirectories && !this.dropTarget_)
687 var destinationEntry = this.destinationEntry_ ||
688 this.currentDirectoryContentEntry;
689 if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry))
691 event.preventDefault();
692 this.paste(event.dataTransfer, destinationEntry,
693 this.selectDropEffect_(event, destinationEntry));
694 this.clearDropTarget_();
698 * Sets the drop target.
700 * @this {FileTransferController}
701 * @param {Element} domElement Target of the drop.
702 * @param {DataTransfer} dataTransfer Data transfer object.
703 * @param {DirectoryEntry} destinationEntry Destination entry.
705 setDropTarget_: function(domElement, dataTransfer, destinationEntry) {
706 if (this.dropTarget_ === domElement)
709 // Remove the old drop target.
710 this.clearDropTarget_();
712 // Set the new drop target.
713 this.dropTarget_ = domElement;
716 !destinationEntry.isDirectory ||
717 !this.canPasteOrDrop_(dataTransfer, destinationEntry)) {
721 // Add accept class if the domElement can accept the drag.
722 domElement.classList.add('accepts');
723 this.destinationEntry_ = destinationEntry;
725 // Start timer changing the directory.
726 this.navigateTimer_ = setTimeout(function() {
727 if (domElement instanceof DirectoryItem)
729 (/** @type {DirectoryItem} */ domElement).doDropTargetAction();
730 this.directoryModel_.changeDirectoryEntry(destinationEntry);
735 * Handles touch start.
737 onTouchStart_: function() {
738 this.touching_ = true;
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;
753 * Clears the drop target.
754 * @this {FileTransferController}
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;
768 * @this {FileTransferController}
769 * @return {boolean} Returns false if {@code <input type="text">} element is
770 * currently active. Otherwise, returns true.
772 isDocumentWideEvent_: function() {
773 return this.document_.activeElement.nodeName.toLowerCase() !== 'input' ||
774 this.document_.activeElement.type.toLowerCase() !== 'text';
778 * @this {FileTransferController}
780 onCopy_: function(event) {
781 if (!this.isDocumentWideEvent_() ||
782 !this.canCopyOrDrag_()) {
785 event.preventDefault();
786 this.cutOrCopy_(event.clipboardData, 'copy');
787 this.notify_('selection-copied');
791 * @this {FileTransferController}
793 onBeforeCopy_: function(event) {
794 if (!this.isDocumentWideEvent_())
797 // queryCommandEnabled returns true if event.defaultPrevented is true.
798 if (this.canCopyOrDrag_())
799 event.preventDefault();
803 * @this {FileTransferController}
804 * @return {boolean} Returns true if all selected files are available to be
807 isAllSelectedFilesAvailable_: function() {
808 if (!this.currentDirectoryContentEntry)
810 var volumeInfo = this.volumeManager_.getVolumeInfo(
811 this.currentDirectoryContentEntry);
814 var isDriveOffline = this.volumeManager_.getDriveConnectionState().type ===
815 VolumeManagerCommon.DriveConnectionType.OFFLINE;
816 if (this.isOnDrive && isDriveOffline && !this.allDriveFilesAvailable)
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.
826 canCopyOrDrag_: function() {
827 return this.isAllSelectedFilesAvailable_() &&
828 this.selectedEntries_.length > 0;
832 * @this {FileTransferController}
834 onCut_: function(event) {
835 if (!this.isDocumentWideEvent_() ||
836 !this.canCutOrDrag_()) {
839 event.preventDefault();
840 this.cutOrCopy_(event.clipboardData, 'move');
841 this.notify_('selection-cut');
845 * @this {FileTransferController}
847 onBeforeCut_: function(event) {
848 if (!this.isDocumentWideEvent_())
850 // queryCommandEnabled returns true if event.defaultPrevented is true.
851 if (this.canCutOrDrag_())
852 event.preventDefault();
856 * @this {FileTransferController}
857 * @return {boolean} Returns true if the current directory is not read only.
859 canCutOrDrag_: function() {
860 return !this.readonly && this.selectedEntries_.length > 0;
864 * @this {FileTransferController}
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;
871 // Need to update here since 'beforepaste' doesn't fire.
872 if (!this.isDocumentWideEvent_() ||
873 !this.canPasteOrDrop_(event.clipboardData, destination)) {
876 event.preventDefault();
877 var effect = this.paste(event.clipboardData, destination);
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', '');
890 * @this {FileTransferController}
892 onBeforePaste_: function(event) {
893 if (!this.isDocumentWideEvent_())
895 // queryCommandEnabled returns true if event.defaultPrevented is true.
896 if (this.canPasteOrDrop_(event.clipboardData,
897 this.currentDirectoryContentEntry)) {
898 event.preventDefault();
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.
909 canPasteOrDrop_: function(dataTransfer, destinationEntry) {
910 if (!destinationEntry)
912 var destinationLocationInfo =
913 this.volumeManager_.getLocationInfo(destinationEntry);
914 if (!destinationLocationInfo || destinationLocationInfo.isReadOnly)
916 if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') === -1)
917 return false; // Unsupported type of content.
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))
929 * Execute paste command.
931 * @this {FileTransferController}
932 * @return {boolean} Returns true, the paste is success. Otherwise, returns
935 queryPasteCommandEnabled: function() {
936 if (!this.isDocumentWideEvent_()) {
940 // HACK(serya): return this.document_.queryCommandEnabled('paste')
943 this.simulateCommand_('paste', function(event) {
944 result = this.canPasteOrDrop_(event.clipboardData,
945 this.currentDirectoryContentEntry);
951 * Allows to simulate commands to get access to clipboard.
953 * @this {FileTransferController}
954 * @param {string} command 'copy', 'cut' or 'paste'.
955 * @param {function} handler Event handler.
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);
966 * @this {FileTransferController}
968 onSelectionChanged_: function(event) {
969 var entries = this.selectedEntries_;
970 var files = this.selectedFileObjects_ = [];
971 this.preloadedThumbnailImagePromise_ = null;
973 var fileEntries = [];
974 for (var i = 0; i < entries.length; i++) {
975 if (entries[i].isFile)
976 fileEntries.push(entries[i]);
978 var containsDirectory = fileEntries.length !== entries.length;
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); });
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
994 this.preloadThumbnailImage_(entries[0]);
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;
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_();
1015 * Obains directory that is displaying now.
1016 * @this {FileTransferController}
1017 * @return {DirectoryEntry} Entry of directry that is displaying now.
1019 get currentDirectoryContentEntry() {
1020 return this.directoryModel_.getCurrentDirEntry();
1024 * @this {FileTransferController}
1025 * @return {boolean} True if the current directory is read only.
1028 return this.directoryModel_.isReadOnly();
1032 * @this {FileTransferController}
1033 * @return {boolean} True if the current directory is on Drive.
1036 var currentDir = this.directoryModel_.getCurrentDirEntry();
1039 var locationInfo = this.volumeManager_.getLocationInfo(currentDir);
1042 return locationInfo.isDriveBased;
1046 * @this {FileTransferController}
1048 notify_: function(eventName) {
1050 // Set timeout to avoid recursive events.
1051 setTimeout(function() {
1052 cr.dispatchSimpleEvent(self, eventName);
1057 * @this {FileTransferController}
1058 * @return {Array.<Entry>} Array of the selected entries.
1060 get selectedEntries_() {
1061 var list = this.directoryModel_.getFileList();
1062 var selectedIndexes = this.directoryModel_.getFileListSelection().
1064 var entries = selectedIndexes.map(function(index) {
1065 return list.item(index);
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);
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.
1086 selectDropEffect_: function(event, destinationEntry) {
1087 if (!destinationEntry)
1089 var destinationLocationInfo =
1090 this.volumeManager_.getLocationInfo(destinationEntry);
1091 if (!destinationLocationInfo)
1093 if (destinationLocationInfo.isReadOnly)
1095 if (util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'move')) {
1096 if (!util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'copy'))
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() &&
1105 if (event.shiftKey) {