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.
24 * @param {ProgressCenter} progressCenter To notify starting copy operation.
27 function FileTransferController(doc,
32 multiProfileShareDialog,
35 this.fileOperationManager_ = fileOperationManager;
36 this.metadataCache_ = metadataCache;
37 this.directoryModel_ = directoryModel;
38 this.volumeManager_ = volumeManager;
39 this.multiProfileShareDialog_ = multiProfileShareDialog;
40 this.progressCenter_ = progressCenter;
42 this.directoryModel_.getFileList().addEventListener(
45 if (this.directoryModel_.getFileListSelection().
46 getIndexSelected(event.index)) {
47 this.onSelectionChanged_();
50 this.directoryModel_.getFileListSelection().addEventListener('change',
51 this.onSelectionChanged_.bind(this));
54 * The array of pending task ID.
55 * @type {Array.<string>}
57 this.pendingTaskIds = [];
60 * Promise to be fulfilled with the thumbnail image of selected file in drag
61 * operation. Used if only one element is selected.
65 this.preloadedThumbnailImagePromise_ = null;
68 * File objects for selected files.
70 * @type {Array.<File>}
73 this.selectedFileObjects_ = [];
77 * @type {DragSelector}
80 this.dragSelector_ = new DragSelector();
83 * Whether a user is touching the device or not.
87 this.touching_ = false;
94 this.taskIdCounter_ = 0;
98 * Size of drag thumbnail for image files.
104 FileTransferController.DRAG_THUMBNAIL_SIZE_ = 64;
106 FileTransferController.prototype = {
107 __proto__: cr.EventTarget.prototype,
110 * @this {FileTransferController}
111 * @param {cr.ui.List} list Items in the list will be draggable.
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);
125 * @this {FileTransferController}
126 * @param {cr.ui.List} list List itself and its directory items will could
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).
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));
143 * @this {FileTransferController}
144 * @param {DirectoryTree} tree Its sub items will could be drop target.
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));
154 * Attach handlers of copy, cut and paste operations to the document.
156 * @this {FileTransferController}
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');
175 * Write the current selection to system clipboard.
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.
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_());
197 for (var i = 0; i < this.selectedFileObjects_.length; i++) {
198 dataTransfer.items.add(this.selectedFileObjects_[i]);
203 * @this {FileTransferController}
204 * @return {Object.<string, string>} Drag and drop global data object.
206 getDragAndDropGlobalData_: function() {
207 if (window[DRAG_AND_DROP_GLOBAL_DATA])
208 return window[DRAG_AND_DROP_GLOBAL_DATA];
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];
220 * Extracts source root URL from the |dataTransfer| object.
222 * @this {FileTransferController}
223 * @param {DataTransfer} dataTransfer DataTransfer object from the event.
224 * @return {string} URL or an empty string (if unknown).
226 getSourceRootURL_: function(dataTransfer) {
227 var sourceRootURL = dataTransfer.getData('fs/sourceRootURL');
229 return sourceRootURL;
231 // |dataTransfer| in protected mode.
232 var globalData = this.getDragAndDropGlobalData_();
234 return globalData.sourceRootURL;
241 * @this {FileTransferController}
242 * @param {DataTransfer} dataTransfer DataTransfer object from the event.
243 * @return {boolean} Returns true when missing some file contents.
245 isMissingFileContents_: function(dataTransfer) {
246 var data = dataTransfer.getData('fs/missingFileContents');
248 // |dataTransfer| in protected mode.
249 var globalData = this.getDragAndDropGlobalData_();
251 data = globalData.missingFileContents;
253 return data === 'true';
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
263 getMultiProfileShareEntries_: function(entries) {
264 // Utility function to concat arrays.
265 var concatArrays = function(arrays) {
266 return Array.prototype.concat.apply([], arrays);
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);
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.
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;
294 // Check child entries.
295 var processDirectoryEntry = function(entry) {
296 return readEntries(entry.createReader());
299 // Read entries from DirectoryReader and call processEntries for the chunk
301 var readEntries = function(reader) {
302 return new Promise(reader.readEntries.bind(reader)).then(
304 if (entries.length > 0) {
306 [processEntries(entries), readEntries(reader)]).
314 'Error happens while reading directory.', error);
319 // Filter entries that is owned by the current user, and call
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);
328 * Queue up a file copy operation based on the current system clipboard.
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".
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
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;
352 var taskId = this.fileOperationManager_.generateTaskId();
354 util.URLsToEntries(sourceURLs).then(function(result) {
355 this.pendingTaskIds.push(taskId);
356 entries = result.entries;
357 failureUrls = result.failureUrls;
358 var item = new ProgressCenterItem();
360 item.type = ProgressItemType.COPY;
361 if (result.entries.length === 1)
362 item.message = strf('COPY_FILE_NAME', result.entries[0].name);
364 item.message = strf('COPY_ITEMS_REMAINING', result.entries.length);
365 this.progressCenter_.updateItem(item);
366 // Check if cross share is needed or not.
367 return this.getMultiProfileShareEntries_(entries);
368 }.bind(this)).then(function(shareEntries) {
369 if (shareEntries.length === 0)
371 return this.multiProfileShareDialog_.show(shareEntries.length > 1).
372 then(function(dialogResult) {
373 if (dialogResult === 'cancel')
374 return Promise.reject('ABORT');
376 // TODO(hirono): Make the loop cancellable.
377 var requestDriveShare = function(index) {
378 if (index >= shareEntries.length)
380 return new Promise(function(fulfill) {
381 chrome.fileManagerPrivate.requestDriveShare(
382 shareEntries[index].toURL(),
385 // TODO(hirono): Check chrome.runtime.lastError here.
388 }).then(requestDriveShare.bind(null, index + 1));
390 return requestDriveShare(0);
392 }.bind(this)).then(function() {
393 // Start the pasting operation.
394 this.fileOperationManager_.paste(
395 entries, destinationEntry, toMove, taskId);
396 this.pendingTaskIds.splice(this.pendingTaskIds.indexOf(taskId), 1);
398 // Publish events for failureUrls.
399 for (var i = 0; i < failureUrls.length; i++) {
401 decodeURIComponent(failureUrls[i].replace(/^.+\//, ''));
402 var event = new Event('source-not-found');
403 event.fileName = fileName;
405 toMove ? ProgressItemType.MOVE : ProgressItemType.COPY;
406 this.dispatchEvent(event);
408 }.bind(this)).catch(function(error) {
409 if (error !== 'ABORT')
410 console.error(error.stack ? error.stack : error);
412 return toMove ? 'move' : 'copy';
416 * Preloads an image thumbnail for the specified file entry.
418 * @this {FileTransferController}
419 * @param {Entry} entry Entry to preload a thumbnail for.
421 preloadThumbnailImage_: function(entry) {
422 var metadataPromise = new Promise(function(fulfill, reject) {
423 this.metadataCache_.getOne(
425 'thumbnail|filesystem',
430 reject('Failed to fetch metadata.');
434 var imagePromise = metadataPromise.then(function(metadata) {
435 return new Promise(function(fulfill, reject) {
436 var loader = new ThumbnailLoader(
437 entry, ThumbnailLoader.LoaderType.Image, metadata);
438 loader.loadDetachedImage(function(result) {
440 fulfill(loader.getImage());
445 imagePromise.then(function(image) {
446 // Store the image so that we can obtain the image synchronously.
447 imagePromise.value = image;
449 console.error(error.stack || error);
452 this.preloadedThumbnailImagePromise_ = imagePromise;
456 * Renders a drag-and-drop thumbnail.
458 * @this {FileTransferController}
459 * @return {HTMLElement} Element containing the thumbnail.
461 renderThumbnail_: function() {
462 var length = this.selectedEntries_.length;
464 var container = this.document_.querySelector('#drag-container');
465 var contents = this.document_.createElement('div');
466 contents.className = 'drag-contents';
467 container.appendChild(contents);
469 // Option 1. Multiple selection, render only a label.
471 var label = this.document_.createElement('div');
472 label.className = 'label';
473 label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
474 contents.appendChild(label);
478 // Option 2. Thumbnail image available, then render it without
480 if (this.preloadedThumbnailImagePromise_ &&
481 this.preloadedThumbnailImagePromise_.value) {
482 var thumbnailImage = this.preloadedThumbnailImagePromise_.value;
484 // Resize the image to canvas.
485 var canvas = document.createElement('canvas');
486 canvas.width = FileTransferController.DRAG_THUMBNAIL_SIZE_;
487 canvas.height = FileTransferController.DRAG_THUMBNAIL_SIZE_;
489 var minScale = Math.min(
490 thumbnailImage.width / canvas.width,
491 thumbnailImage.height / canvas.height);
492 var srcWidth = Math.min(canvas.width * minScale, thumbnailImage.width);
493 var srcHeight = Math.min(canvas.height * minScale, thumbnailImage.height);
495 var context = canvas.getContext('2d');
496 context.drawImage(thumbnailImage,
497 (thumbnailImage.width - srcWidth) / 2,
498 (thumbnailImage.height - srcHeight) / 2,
505 contents.classList.add('for-image');
506 contents.appendChild(canvas);
510 // Option 3. Thumbnail not available. Render an icon and a label.
511 var entry = this.selectedEntries_[0];
512 var icon = this.document_.createElement('div');
513 icon.className = 'detail-icon';
514 icon.setAttribute('file-type-icon', FileType.getIcon(entry));
515 contents.appendChild(icon);
516 var label = this.document_.createElement('div');
517 label.className = 'label';
518 label.textContent = entry.name;
519 contents.appendChild(label);
524 * @this {FileTransferController}
525 * @param {cr.ui.List} list Drop target list
526 * @param {Event} event A dragstart event of DOM.
528 onDragStart_: function(list, event) {
529 // Check if a drag selection should be initiated or not.
530 if (list.shouldStartDragSelection(event)) {
531 event.preventDefault();
532 // If this drag operation is initiated by mouse, start selecting area.
534 this.dragSelector_.startDragSelection(list, event);
539 if (!this.selectedEntries_.length) {
540 event.preventDefault();
544 var dt = event.dataTransfer;
545 var canCopy = this.canCopyOrDrag_(dt);
546 var canCut = this.canCutOrDrag_(dt);
547 if (canCopy || canCut) {
548 if (canCopy && canCut) {
549 this.cutOrCopy_(dt, 'all');
550 } else if (canCopy) {
551 this.cutOrCopy_(dt, 'copyLink');
553 this.cutOrCopy_(dt, 'move');
556 event.preventDefault();
560 var dragThumbnail = this.renderThumbnail_();
561 dt.setDragImage(dragThumbnail, 0, 0);
563 window[DRAG_AND_DROP_GLOBAL_DATA] = {
564 sourceRootURL: dt.getData('fs/sourceRootURL'),
565 missingFileContents: dt.getData('fs/missingFileContents')
570 * @this {FileTransferController}
571 * @param {cr.ui.List} list Drop target list.
572 * @param {Event} event A dragend event of DOM.
574 onDragEnd_: function(list, event) {
575 // TODO(fukino): This is workaround for crbug.com/373125.
576 // This should be removed after the bug is fixed.
577 this.touching_ = false;
579 var container = this.document_.querySelector('#drag-container');
580 container.textContent = '';
581 this.clearDropTarget_();
582 delete window[DRAG_AND_DROP_GLOBAL_DATA];
586 * @this {FileTransferController}
587 * @param {boolean} onlyIntoDirectories True if the drag is only into
589 * @param {cr.ui.List} list Drop target list.
590 * @param {Event} event A dragover event of DOM.
592 onDragOver_: function(onlyIntoDirectories, list, event) {
593 event.preventDefault();
594 var entry = this.destinationEntry_ ||
595 (!onlyIntoDirectories && this.currentDirectoryContentEntry);
596 event.dataTransfer.dropEffect = this.selectDropEffect_(event, entry);
597 event.preventDefault();
601 * @this {FileTransferController}
602 * @param {cr.ui.List} list Drop target list.
603 * @param {Event} event A dragenter event of DOM.
605 onDragEnterFileList_: function(list, event) {
606 event.preventDefault(); // Required to prevent the cursor flicker.
607 this.lastEnteredTarget_ = event.target;
608 var item = list.getListItemAncestor(event.target);
609 item = item && list.isItem(item) ? item : null;
610 if (item === this.dropTarget_)
613 var entry = item && list.dataModel.item(item.listIndex);
615 this.setDropTarget_(item, event.dataTransfer, entry);
617 this.clearDropTarget_();
621 * @this {FileTransferController}
622 * @param {DirectoryTree} tree Drop target tree.
623 * @param {Event} event A dragenter event of DOM.
625 onDragEnterTree_: function(tree, event) {
626 event.preventDefault(); // Required to prevent the cursor flicker.
627 this.lastEnteredTarget_ = event.target;
628 var item = event.target;
629 while (item && !(item instanceof cr.ui.TreeItem)) {
630 item = item.parentNode;
633 if (item === this.dropTarget_)
636 var entry = item && item.entry;
638 this.setDropTarget_(item, event.dataTransfer, entry);
640 this.clearDropTarget_();
645 * @this {FileTransferController}
646 * @param {cr.ui.List} list Drop target list.
647 * @param {Event} event A dragleave event of DOM.
649 onDragLeave_: function(list, event) {
650 // If mouse moves from one element to another the 'dragenter'
651 // event for the new element comes before the 'dragleave' event for
652 // the old one. In this case event.target !== this.lastEnteredTarget_
653 // and handler of the 'dragenter' event has already caried of
654 // drop target. So event.target === this.lastEnteredTarget_
655 // could only be if mouse goes out of listened element.
656 if (event.target === this.lastEnteredTarget_) {
657 this.clearDropTarget_();
658 this.lastEnteredTarget_ = null;
663 * @this {FileTransferController}
664 * @param {boolean} onlyIntoDirectories True if the drag is only into
666 * @param {Event} event A dragleave event of DOM.
668 onDrop_: function(onlyIntoDirectories, event) {
669 if (onlyIntoDirectories && !this.dropTarget_)
671 var destinationEntry = this.destinationEntry_ ||
672 this.currentDirectoryContentEntry;
673 if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry))
675 event.preventDefault();
676 this.paste(event.dataTransfer, destinationEntry,
677 this.selectDropEffect_(event, destinationEntry));
678 this.clearDropTarget_();
682 * Sets the drop target.
684 * @this {FileTransferController}
685 * @param {Element} domElement Target of the drop.
686 * @param {DataTransfer} dataTransfer Data transfer object.
687 * @param {DirectoryEntry} destinationEntry Destination entry.
689 setDropTarget_: function(domElement, dataTransfer, destinationEntry) {
690 if (this.dropTarget_ === domElement)
693 // Remove the old drop target.
694 this.clearDropTarget_();
696 // Set the new drop target.
697 this.dropTarget_ = domElement;
700 !destinationEntry.isDirectory ||
701 !this.canPasteOrDrop_(dataTransfer, destinationEntry)) {
705 // Add accept class if the domElement can accept the drag.
706 domElement.classList.add('accepts');
707 this.destinationEntry_ = destinationEntry;
709 // Start timer changing the directory.
710 this.navigateTimer_ = setTimeout(function() {
711 if (domElement instanceof DirectoryItem)
713 (/** @type {DirectoryItem} */ domElement).doDropTargetAction();
714 this.directoryModel_.changeDirectoryEntry(destinationEntry);
719 * Handles touch start.
721 onTouchStart_: function() {
722 this.touching_ = true;
728 onTouchEnd_: function(event) {
729 // TODO(fukino): We have to check if event.touches.length be 0 to support
730 // multi-touch operations, but event.touches has incorrect value by a bug
731 // (crbug.com/373125).
732 // After the bug is fixed, we should check event.touches.
733 this.touching_ = false;
737 * Clears the drop target.
738 * @this {FileTransferController}
740 clearDropTarget_: function() {
741 if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts'))
742 this.dropTarget_.classList.remove('accepts');
743 this.dropTarget_ = null;
744 this.destinationEntry_ = null;
745 if (this.navigateTimer_ !== undefined) {
746 clearTimeout(this.navigateTimer_);
747 this.navigateTimer_ = undefined;
752 * @this {FileTransferController}
753 * @return {boolean} Returns false if {@code <input type="text">} element is
754 * currently active. Otherwise, returns true.
756 isDocumentWideEvent_: function() {
757 return this.document_.activeElement.nodeName.toLowerCase() !== 'input' ||
758 this.document_.activeElement.type.toLowerCase() !== 'text';
762 * @this {FileTransferController}
764 onCopy_: function(event) {
765 if (!this.isDocumentWideEvent_() ||
766 !this.canCopyOrDrag_()) {
769 event.preventDefault();
770 this.cutOrCopy_(event.clipboardData, 'copy');
771 this.notify_('selection-copied');
775 * @this {FileTransferController}
777 onBeforeCopy_: function(event) {
778 if (!this.isDocumentWideEvent_())
781 // queryCommandEnabled returns true if event.defaultPrevented is true.
782 if (this.canCopyOrDrag_())
783 event.preventDefault();
787 * @this {FileTransferController}
788 * @return {boolean} Returns true if all selected files are available to be
791 isAllSelectedFilesAvailable_: function() {
792 if (!this.currentDirectoryContentEntry)
794 var volumeInfo = this.volumeManager_.getVolumeInfo(
795 this.currentDirectoryContentEntry);
798 var isDriveOffline = this.volumeManager_.getDriveConnectionState().type ===
799 VolumeManagerCommon.DriveConnectionType.OFFLINE;
800 if (this.isOnDrive && isDriveOffline && !this.allDriveFilesAvailable)
806 * @this {FileTransferController}
807 * @return {boolean} Returns true if some files are selected and all the file
808 * on drive is available to be copied. Otherwise, returns false.
810 canCopyOrDrag_: function() {
811 return this.isAllSelectedFilesAvailable_() &&
812 this.selectedEntries_.length > 0;
816 * @this {FileTransferController}
818 onCut_: function(event) {
819 if (!this.isDocumentWideEvent_() ||
820 !this.canCutOrDrag_()) {
823 event.preventDefault();
824 this.cutOrCopy_(event.clipboardData, 'move');
825 this.notify_('selection-cut');
829 * @this {FileTransferController}
831 onBeforeCut_: function(event) {
832 if (!this.isDocumentWideEvent_())
834 // queryCommandEnabled returns true if event.defaultPrevented is true.
835 if (this.canCutOrDrag_())
836 event.preventDefault();
840 * @this {FileTransferController}
841 * @return {boolean} Returns true if the current directory is not read only.
843 canCutOrDrag_: function() {
844 return !this.readonly && this.selectedEntries_.length > 0;
848 * @this {FileTransferController}
850 onPaste_: function(event) {
851 // If the event has destDirectory property, paste files into the directory.
852 // This occurs when the command fires from menu item 'Paste into folder'.
853 var destination = event.destDirectory || this.currentDirectoryContentEntry;
855 // Need to update here since 'beforepaste' doesn't fire.
856 if (!this.isDocumentWideEvent_() ||
857 !this.canPasteOrDrop_(event.clipboardData, destination)) {
860 event.preventDefault();
861 var effect = this.paste(event.clipboardData, destination);
863 // On cut, we clear the clipboard after the file is pasted/moved so we don't
864 // try to move/delete the original file again.
865 if (effect === 'move') {
866 this.simulateCommand_('cut', function(event) {
867 event.preventDefault();
868 event.clipboardData.setData('fs/clear', '');
874 * @this {FileTransferController}
876 onBeforePaste_: function(event) {
877 if (!this.isDocumentWideEvent_())
879 // queryCommandEnabled returns true if event.defaultPrevented is true.
880 if (this.canPasteOrDrop_(event.clipboardData,
881 this.currentDirectoryContentEntry)) {
882 event.preventDefault();
887 * @this {FileTransferController}
888 * @param {DataTransfer} dataTransfer Data transfer object.
889 * @param {DirectoryEntry} destinationEntry Destination entry.
890 * @return {boolean} Returns true if items stored in {@code dataTransfer} can
891 * be pasted to {@code destinationEntry}. Otherwise, returns false.
893 canPasteOrDrop_: function(dataTransfer, destinationEntry) {
894 if (!destinationEntry)
896 var destinationLocationInfo =
897 this.volumeManager_.getLocationInfo(destinationEntry);
898 if (!destinationLocationInfo || destinationLocationInfo.isReadOnly)
900 if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') === -1)
901 return false; // Unsupported type of content.
903 // Copying between different sources requires all files to be available.
904 if (this.getSourceRootURL_(dataTransfer) !==
905 destinationLocationInfo.volumeInfo.fileSystem.root.toURL() &&
906 this.isMissingFileContents_(dataTransfer))
913 * Execute paste command.
915 * @this {FileTransferController}
916 * @return {boolean} Returns true, the paste is success. Otherwise, returns
919 queryPasteCommandEnabled: function() {
920 if (!this.isDocumentWideEvent_()) {
924 // HACK(serya): return this.document_.queryCommandEnabled('paste')
927 this.simulateCommand_('paste', function(event) {
928 result = this.canPasteOrDrop_(event.clipboardData,
929 this.currentDirectoryContentEntry);
935 * Allows to simulate commands to get access to clipboard.
937 * @this {FileTransferController}
938 * @param {string} command 'copy', 'cut' or 'paste'.
939 * @param {function} handler Event handler.
941 simulateCommand_: function(command, handler) {
942 var iframe = this.document_.querySelector('#command-dispatcher');
943 var doc = iframe.contentDocument;
944 doc.addEventListener(command, handler);
945 doc.execCommand(command);
946 doc.removeEventListener(command, handler);
950 * @this {FileTransferController}
952 onSelectionChanged_: function(event) {
953 var entries = this.selectedEntries_;
954 var files = this.selectedFileObjects_ = [];
955 this.preloadedThumbnailImagePromise_ = null;
957 var fileEntries = [];
958 for (var i = 0; i < entries.length; i++) {
959 if (entries[i].isFile)
960 fileEntries.push(entries[i]);
962 var containsDirectory = fileEntries.length !== entries.length;
964 // File object must be prepeared in advance for clipboard operations
965 // (copy, paste and drag). DataTransfer object closes for write after
966 // returning control from that handlers so they may not have
967 // asynchronous operations.
968 if (!containsDirectory) {
969 for (var i = 0; i < fileEntries.length; i++) {
970 fileEntries[i].file(function(file) { files.push(file); });
974 if (entries.length === 1) {
975 // For single selection, the dragged element is created in advance,
976 // otherwise an image may not be loaded at the time the 'dragstart' event
978 this.preloadThumbnailImage_(entries[0]);
981 if (this.isOnDrive) {
982 this.allDriveFilesAvailable = false;
983 this.metadataCache_.get(entries, 'external', function(props) {
984 // We consider directories not available offline for the purposes of
985 // file transfer since we cannot afford to recursive traversal.
986 this.allDriveFilesAvailable =
987 !containsDirectory &&
988 props.filter(function(p) {
989 return !p.availableOffline;
991 // |Copy| is the only menu item affected by allDriveFilesAvailable.
992 // It could be open right now, update its UI.
993 this.copyCommand_.disabled = !this.canCopyOrDrag_();
999 * Obains directory that is displaying now.
1000 * @this {FileTransferController}
1001 * @return {DirectoryEntry} Entry of directry that is displaying now.
1003 get currentDirectoryContentEntry() {
1004 return this.directoryModel_.getCurrentDirEntry();
1008 * @this {FileTransferController}
1009 * @return {boolean} True if the current directory is read only.
1012 return this.directoryModel_.isReadOnly();
1016 * @this {FileTransferController}
1017 * @return {boolean} True if the current directory is on Drive.
1020 var currentDir = this.directoryModel_.getCurrentDirEntry();
1023 var locationInfo = this.volumeManager_.getLocationInfo(currentDir);
1026 return locationInfo.isDriveBased;
1030 * @this {FileTransferController}
1032 notify_: function(eventName) {
1034 // Set timeout to avoid recursive events.
1035 setTimeout(function() {
1036 cr.dispatchSimpleEvent(self, eventName);
1041 * @this {FileTransferController}
1042 * @return {Array.<Entry>} Array of the selected entries.
1044 get selectedEntries_() {
1045 var list = this.directoryModel_.getFileList();
1046 var selectedIndexes = this.directoryModel_.getFileListSelection().
1048 var entries = selectedIndexes.map(function(index) {
1049 return list.item(index);
1052 // TODO(serya): Diagnostics for http://crbug/129642
1053 if (entries.indexOf(undefined) !== -1) {
1054 var index = entries.indexOf(undefined);
1055 entries = entries.filter(function(e) { return !!e; });
1056 console.error('Invalid selection found: list items: ', list.length,
1057 'wrong indexe value: ', selectedIndexes[index],
1058 'Stack trace: ', new Error().stack);
1064 * @param {Event} event Drag event.
1065 * @param {DirectoryEntry} destinationEntry Destination entry.
1066 * @this {FileTransferController}
1067 * @return {string} Returns the appropriate drop query type ('none', 'move'
1068 * or copy') to the current modifiers status and the destination.
1070 selectDropEffect_: function(event, destinationEntry) {
1071 if (!destinationEntry)
1073 var destinationLocationInfo =
1074 this.volumeManager_.getLocationInfo(destinationEntry);
1075 if (!destinationLocationInfo)
1077 if (destinationLocationInfo.isReadOnly)
1079 if (util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'move')) {
1080 if (!util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'copy'))
1082 // TODO(mtomasz): Use volumeId instead of comparing roots, as soon as
1083 // volumeId gets unique.
1084 if (this.getSourceRootURL_(event.dataTransfer) ===
1085 destinationLocationInfo.volumeInfo.fileSystem.root.toURL() &&
1089 if (event.shiftKey) {