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 * DOM element to represent selected file in drag operation. Used if only
51 * one element is selected.
55 this.preloadedThumbnailImageNode_ = 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;
80 FileTransferController.prototype = {
81 __proto__: cr.EventTarget.prototype,
84 * @this {FileTransferController}
85 * @param {cr.ui.List} list Items in the list will be draggable.
87 attachDragSource: function(list) {
88 list.style.webkitUserDrag = 'element';
89 list.addEventListener('dragstart', this.onDragStart_.bind(this, list));
90 list.addEventListener('dragend', this.onDragEnd_.bind(this, list));
91 list.addEventListener('touchstart', this.onTouchStart_.bind(this));
92 list.ownerDocument.addEventListener(
93 'touchend', this.onTouchEnd_.bind(this), true);
94 list.ownerDocument.addEventListener(
95 'touchcancel', this.onTouchEnd_.bind(this), true);
99 * @this {FileTransferController}
100 * @param {cr.ui.List} list List itself and its directory items will could
102 * @param {boolean=} opt_onlyIntoDirectories If true only directory list
103 * items could be drop targets. Otherwise any other place of the list
104 * accetps files (putting it into the current directory).
106 attachFileListDropTarget: function(list, opt_onlyIntoDirectories) {
107 list.addEventListener('dragover', this.onDragOver_.bind(this,
108 !!opt_onlyIntoDirectories, list));
109 list.addEventListener('dragenter',
110 this.onDragEnterFileList_.bind(this, list));
111 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list));
112 list.addEventListener('drop',
113 this.onDrop_.bind(this, !!opt_onlyIntoDirectories));
117 * @this {FileTransferController}
118 * @param {DirectoryTree} tree Its sub items will could be drop target.
120 attachTreeDropTarget: function(tree) {
121 tree.addEventListener('dragover', this.onDragOver_.bind(this, true, tree));
122 tree.addEventListener('dragenter', this.onDragEnterTree_.bind(this, tree));
123 tree.addEventListener('dragleave', this.onDragLeave_.bind(this, tree));
124 tree.addEventListener('drop', this.onDrop_.bind(this, true));
128 * @this {FileTransferController}
129 * @param {NavigationList} tree Its sub items will could be drop target.
131 attachNavigationListDropTarget: function(list) {
132 list.addEventListener('dragover',
133 this.onDragOver_.bind(this, true /* onlyIntoDirectories */, list));
134 list.addEventListener('dragenter',
135 this.onDragEnterVolumesList_.bind(this, list));
136 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list));
137 list.addEventListener('drop',
138 this.onDrop_.bind(this, true /* onlyIntoDirectories */));
142 * Attach handlers of copy, cut and paste operations to the document.
144 * @this {FileTransferController}
146 attachCopyPasteHandlers: function() {
147 this.document_.addEventListener('beforecopy',
148 this.onBeforeCopy_.bind(this));
149 this.document_.addEventListener('copy',
150 this.onCopy_.bind(this));
151 this.document_.addEventListener('beforecut',
152 this.onBeforeCut_.bind(this));
153 this.document_.addEventListener('cut',
154 this.onCut_.bind(this));
155 this.document_.addEventListener('beforepaste',
156 this.onBeforePaste_.bind(this));
157 this.document_.addEventListener('paste',
158 this.onPaste_.bind(this));
159 this.copyCommand_ = this.document_.querySelector('command#copy');
163 * Write the current selection to system clipboard.
165 * @this {FileTransferController}
166 * @param {DataTransfer} dataTransfer DataTransfer from the event.
167 * @param {string} effectAllowed Value must be valid for the
168 * |dataTransfer.effectAllowed| property ('move', 'copy', 'copyMove').
170 cutOrCopy_: function(dataTransfer, effectAllowed) {
171 // Existence of the volumeInfo is checked in canXXX methods.
172 var volumeInfo = this.volumeManager_.getVolumeInfo(
173 this.currentDirectoryContentEntry);
174 // Tag to check it's filemanager data.
175 dataTransfer.setData('fs/tag', 'filemanager-data');
176 dataTransfer.setData('fs/sourceRootURL',
177 volumeInfo.fileSystem.root.toURL());
178 var sourceURLs = util.entriesToURLs(this.selectedEntries_);
179 dataTransfer.setData('fs/sources', sourceURLs.join('\n'));
180 dataTransfer.effectAllowed = effectAllowed;
181 dataTransfer.setData('fs/effectallowed', effectAllowed);
182 dataTransfer.setData('fs/missingFileContents',
183 !this.isAllSelectedFilesAvailable_());
185 for (var i = 0; i < this.selectedFileObjects_.length; i++) {
186 dataTransfer.items.add(this.selectedFileObjects_[i]);
191 * @this {FileTransferController}
192 * @return {Object.<string, string>} Drag and drop global data object.
194 getDragAndDropGlobalData_: function() {
195 if (window[DRAG_AND_DROP_GLOBAL_DATA])
196 return window[DRAG_AND_DROP_GLOBAL_DATA];
198 // Dragging from other tabs/windows.
199 var views = chrome && chrome.extension ? chrome.extension.getViews() : [];
200 for (var i = 0; i < views.length; i++) {
201 if (views[i][DRAG_AND_DROP_GLOBAL_DATA])
202 return views[i][DRAG_AND_DROP_GLOBAL_DATA];
208 * Extracts source root URL from the |dataTransfer| object.
210 * @this {FileTransferController}
211 * @param {DataTransfer} dataTransfer DataTransfer object from the event.
212 * @return {string} URL or an empty string (if unknown).
214 getSourceRootURL_: function(dataTransfer) {
215 var sourceRootURL = dataTransfer.getData('fs/sourceRootURL');
217 return sourceRootURL;
219 // |dataTransfer| in protected mode.
220 var globalData = this.getDragAndDropGlobalData_();
222 return globalData.sourceRootURL;
229 * @this {FileTransferController}
230 * @param {DataTransfer} dataTransfer DataTransfer object from the event.
231 * @return {boolean} Returns true when missing some file contents.
233 isMissingFileContents_: function(dataTransfer) {
234 var data = dataTransfer.getData('fs/missingFileContents');
236 // |dataTransfer| in protected mode.
237 var globalData = this.getDragAndDropGlobalData_();
239 data = globalData.missingFileContents;
241 return data === 'true';
245 * Obtains entries that need to share with me.
246 * The method also observers child entries of the given entries.
247 * @param {Array.<Entries>} entries Entries.
248 * @return {Promise} Promise to be fulfilled with the entries that need to
251 getMultiProfileShareEntries_: function(entries) {
252 // Utility function to concat arrays.
253 var concatArrays = function(arrays) {
254 return Array.prototype.concat.apply([], arrays);
257 // Call processEntry for each item of entries.
258 var processEntries = function(entries) {
259 var files = entries.filter(function(entry) {return entry.isFile;});
260 var dirs = entries.filter(function(entry) {return !entry.isFile;});
261 var promises = dirs.map(processDirectoryEntry);
262 if (files.length > 0)
263 promises.push(processFileEntries(files));
264 return Promise.all(promises).then(concatArrays);
267 // Check all file entries and keeps only those need sharing operation.
268 var processFileEntries = function(entries) {
269 return new Promise(function(callback) {
270 var urls = util.entriesToURLs(entries);
271 chrome.fileBrowserPrivate.getDriveEntryProperties(urls, callback);
273 then(function(metadatas) {
274 return entries.filter(function(entry, i) {
275 var metadata = metadatas[i];
276 return metadata && metadata.isHosted && !metadata.sharedWithMe;
281 // Check child entries.
282 var processDirectoryEntry = function(entry) {
283 return readEntries(entry.createReader());
286 // Read entries from DirectoryReader and call processEntries for the chunk
288 var readEntries = function(reader) {
289 return new Promise(reader.readEntries.bind(reader)).then(
291 if (entries.length > 0) {
293 [processEntries(entries), readEntries(reader)]).
301 'Error happens while reading directory.', error);
306 // Filter entries that is owned by the current user, and call
308 return processEntries(entries.filter(function(entry) {
309 // If the volumeInfo is found, the entry belongs to the current user.
310 return !this.volumeManager_.getVolumeInfo(entry);
315 * Queue up a file copy operation based on the current system clipboard.
317 * @this {FileTransferController}
318 * @param {DataTransfer} dataTransfer System data transfer object.
319 * @param {DirectoryEntry=} opt_destinationEntry Paste destination.
320 * @param {string=} opt_effect Desired drop/paste effect. Could be
321 * 'move'|'copy' (default is copy). Ignored if conflicts with
322 * |dataTransfer.effectAllowed|.
323 * @return {string} Either "copy" or "move".
325 paste: function(dataTransfer, opt_destinationEntry, opt_effect) {
326 var sourceURLs = dataTransfer.getData('fs/sources') ?
327 dataTransfer.getData('fs/sources').split('\n') : [];
328 // effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers
330 var effectAllowed = dataTransfer.effectAllowed !== 'uninitialized' ?
331 dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed');
332 var toMove = effectAllowed === 'move' ||
333 (effectAllowed === 'copyMove' && opt_effect === 'move');
334 var destinationEntry =
335 opt_destinationEntry || this.currentDirectoryContentEntry;
339 util.URLsToEntries(sourceURLs).
340 then(function(result) {
341 entries = result.entries;
342 failureUrls = result.failureUrls;
343 // Check if cross share is needed or not.
344 return this.getMultiProfileShareEntries_(entries);
346 then(function(shareEntries) {
347 if (shareEntries.length === 0)
349 return this.multiProfileShareDialog_.show(shareEntries.length > 1).
350 then(function(dialogResult) {
351 if (dialogResult === 'cancel')
352 return Promise.reject('ABORT');
354 // TODO(hirono): Make the loop cancellable.
355 var requestDriveShare = function(index) {
356 if (index >= shareEntries.length)
358 return new Promise(function(fulfill) {
359 chrome.fileBrowserPrivate.requestDriveShare(
360 shareEntries[index].toURL(),
363 // TODO(hirono): Check chrome.runtime.lastError here.
366 }).then(requestDriveShare.bind(null, index + 1));
368 return requestDriveShare(0);
372 // Start the pasting operation.
373 this.fileOperationManager_.paste(
374 entries, destinationEntry, toMove);
376 // Publish events for failureUrls.
377 for (var i = 0; i < failureUrls.length; i++) {
379 decodeURIComponent(failureUrls[i].replace(/^.+\//, ''));
380 var event = new Event('source-not-found');
381 event.fileName = fileName;
383 toMove ? ProgressItemType.MOVE : ProgressItemType.COPY;
384 this.dispatchEvent(event);
387 catch(function(error) {
388 if (error !== 'ABORT')
389 console.error(error.stack ? error.stack : error);
391 return toMove ? 'move' : 'copy';
395 * Preloads an image thumbnail for the specified file entry.
397 * @this {FileTransferController}
398 * @param {Entry} entry Entry to preload a thumbnail for.
400 preloadThumbnailImage_: function(entry) {
401 var metadataTypes = 'thumbnail|filesystem';
402 var thumbnailContainer = this.document_.createElement('div');
403 this.preloadedThumbnailImageNode_ = thumbnailContainer;
404 this.preloadedThumbnailImageNode_.className = 'img-container';
405 this.metadataCache_.get(
409 new ThumbnailLoader(entry,
410 ThumbnailLoader.LoaderType.IMAGE,
412 load(thumbnailContainer,
413 ThumbnailLoader.FillMode.FILL);
418 * Renders a drag-and-drop thumbnail.
420 * @this {FileTransferController}
421 * @return {HTMLElement} Element containing the thumbnail.
423 renderThumbnail_: function() {
424 var length = this.selectedEntries_.length;
426 var container = this.document_.querySelector('#drag-container');
427 var contents = this.document_.createElement('div');
428 contents.className = 'drag-contents';
429 container.appendChild(contents);
432 if (this.preloadedThumbnailImageNode_)
433 thumbnailImage = this.preloadedThumbnailImageNode_.querySelector('img');
435 // Option 1. Multiple selection, render only a label.
437 var label = this.document_.createElement('div');
438 label.className = 'label';
439 label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
440 contents.appendChild(label);
444 // Option 2. Thumbnail image available, then render it without
446 if (thumbnailImage) {
447 thumbnailImage.classList.add('drag-thumbnail');
448 contents.classList.add('for-image');
449 contents.appendChild(this.preloadedThumbnailImageNode_);
453 // Option 3. Thumbnail not available. Render an icon and a label.
454 var entry = this.selectedEntries_[0];
455 var icon = this.document_.createElement('div');
456 icon.className = 'detail-icon';
457 icon.setAttribute('file-type-icon', FileType.getIcon(entry));
458 contents.appendChild(icon);
459 var label = this.document_.createElement('div');
460 label.className = 'label';
461 label.textContent = entry.name;
462 contents.appendChild(label);
467 * @this {FileTransferController}
468 * @param {cr.ui.List} list Drop target list
469 * @param {Event} event A dragstart event of DOM.
471 onDragStart_: function(list, event) {
472 // If a user is touching, Files.app does not receive drag operations.
473 if (this.touching_) {
474 event.preventDefault();
478 // Check if a drag selection should be initiated or not.
479 if (list.shouldStartDragSelection(event)) {
480 this.dragSelector_.startDragSelection(list, event);
485 if (!this.selectedEntries_.length) {
486 event.preventDefault();
490 var dt = event.dataTransfer;
491 var canCopy = this.canCopyOrDrag_(dt);
492 var canCut = this.canCutOrDrag_(dt);
493 if (canCopy || canCut) {
494 if (canCopy && canCut) {
495 this.cutOrCopy_(dt, 'copyMove');
496 } else if (canCopy) {
497 this.cutOrCopy_(dt, 'copy');
499 this.cutOrCopy_(dt, 'move');
502 event.preventDefault();
506 var dragThumbnail = this.renderThumbnail_();
507 dt.setDragImage(dragThumbnail, 1000, 1000);
509 window[DRAG_AND_DROP_GLOBAL_DATA] = {
510 sourceRootURL: dt.getData('fs/sourceRootURL'),
511 missingFileContents: dt.getData('fs/missingFileContents'),
516 * @this {FileTransferController}
517 * @param {cr.ui.List} list Drop target list.
518 * @param {Event} event A dragend event of DOM.
520 onDragEnd_: function(list, event) {
521 var container = this.document_.querySelector('#drag-container');
522 container.textContent = '';
523 this.clearDropTarget_();
524 delete window[DRAG_AND_DROP_GLOBAL_DATA];
528 * @this {FileTransferController}
529 * @param {boolean} onlyIntoDirectories True if the drag is only into
531 * @param {cr.ui.List} list Drop target list.
532 * @param {Event} event A dragover event of DOM.
534 onDragOver_: function(onlyIntoDirectories, list, event) {
535 event.preventDefault();
536 var entry = this.destinationEntry_ ||
537 (!onlyIntoDirectories && this.currentDirectoryContentEntry);
538 event.dataTransfer.dropEffect = this.selectDropEffect_(event, entry);
539 event.preventDefault();
543 * @this {FileTransferController}
544 * @param {cr.ui.List} list Drop target list.
545 * @param {Event} event A dragenter event of DOM.
547 onDragEnterFileList_: function(list, event) {
548 event.preventDefault(); // Required to prevent the cursor flicker.
549 this.lastEnteredTarget_ = event.target;
550 var item = list.getListItemAncestor(event.target);
551 item = item && list.isItem(item) ? item : null;
552 if (item === this.dropTarget_)
555 var entry = item && list.dataModel.item(item.listIndex);
557 this.setDropTarget_(item, event.dataTransfer, entry);
559 this.clearDropTarget_();
563 * @this {FileTransferController}
564 * @param {DirectoryTree} tree Drop target tree.
565 * @param {Event} event A dragenter event of DOM.
567 onDragEnterTree_: function(tree, event) {
568 event.preventDefault(); // Required to prevent the cursor flicker.
569 this.lastEnteredTarget_ = event.target;
570 var item = event.target;
571 while (item && !(item instanceof DirectoryItem)) {
572 item = item.parentNode;
575 if (item === this.dropTarget_)
578 var entry = item && item.entry;
580 this.setDropTarget_(item, event.dataTransfer, entry);
582 this.clearDropTarget_();
587 * @this {FileTransferController}
588 * @param {NavigationList} list Drop target list.
589 * @param {Event} event A dragenter event of DOM.
591 onDragEnterVolumesList_: function(list, event) {
592 event.preventDefault(); // Required to prevent the cursor flicker.
594 this.lastEnteredTarget_ = event.target;
595 var item = list.getListItemAncestor(event.target);
596 item = item && list.isItem(item) ? item : null;
597 if (item === this.dropTarget_)
600 var modelItem = item && list.dataModel.item(item.listIndex);
601 if (modelItem && modelItem.isShortcut) {
602 this.setDropTarget_(item, event.dataTransfer, modelItem.entry);
605 if (modelItem && modelItem.isVolume && modelItem.volumeInfo.displayRoot) {
607 item, event.dataTransfer, modelItem.volumeInfo.displayRoot);
611 this.clearDropTarget_();
615 * @this {FileTransferController}
616 * @param {cr.ui.List} list Drop target list.
617 * @param {Event} event A dragleave event of DOM.
619 onDragLeave_: function(list, event) {
620 // If mouse moves from one element to another the 'dragenter'
621 // event for the new element comes before the 'dragleave' event for
622 // the old one. In this case event.target !== this.lastEnteredTarget_
623 // and handler of the 'dragenter' event has already caried of
624 // drop target. So event.target === this.lastEnteredTarget_
625 // could only be if mouse goes out of listened element.
626 if (event.target === this.lastEnteredTarget_) {
627 this.clearDropTarget_();
628 this.lastEnteredTarget_ = null;
633 * @this {FileTransferController}
634 * @param {boolean} onlyIntoDirectories True if the drag is only into
636 * @param {Event} event A dragleave event of DOM.
638 onDrop_: function(onlyIntoDirectories, event) {
639 if (onlyIntoDirectories && !this.dropTarget_)
641 var destinationEntry = this.destinationEntry_ ||
642 this.currentDirectoryContentEntry;
643 if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry))
645 event.preventDefault();
646 this.paste(event.dataTransfer, destinationEntry,
647 this.selectDropEffect_(event, destinationEntry));
648 this.clearDropTarget_();
652 * Sets the drop target.
654 * @this {FileTransferController}
655 * @param {Element} domElement Target of the drop.
656 * @param {DataTransfer} dataTransfer Data transfer object.
657 * @param {DirectoryEntry} destinationEntry Destination entry.
659 setDropTarget_: function(domElement, dataTransfer, destinationEntry) {
660 if (this.dropTarget_ === domElement)
663 // Remove the old drop target.
664 this.clearDropTarget_();
666 // Set the new drop target.
667 this.dropTarget_ = domElement;
670 !destinationEntry.isDirectory ||
671 !this.canPasteOrDrop_(dataTransfer, destinationEntry)) {
675 // Add accept class if the domElement can accept the drag.
676 domElement.classList.add('accepts');
677 this.destinationEntry_ = destinationEntry;
679 // Start timer changing the directory.
680 this.navigateTimer_ = setTimeout(function() {
681 if (domElement instanceof DirectoryItem)
683 (/** @type {DirectoryItem} */ domElement).doDropTargetAction();
684 this.directoryModel_.changeDirectoryEntry(destinationEntry);
689 * Handles touch start.
691 onTouchStart_: function() {
692 this.touching_ = true;
698 onTouchEnd_: function(event) {
699 if (event.touches.length === 0)
700 this.touching_ = false;
704 * Clears the drop target.
705 * @this {FileTransferController}
707 clearDropTarget_: function() {
708 if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts'))
709 this.dropTarget_.classList.remove('accepts');
710 this.dropTarget_ = null;
711 this.destinationEntry_ = null;
712 if (this.navigateTimer_ !== undefined) {
713 clearTimeout(this.navigateTimer_);
714 this.navigateTimer_ = undefined;
719 * @this {FileTransferController}
720 * @return {boolean} Returns false if {@code <input type="text">} element is
721 * currently active. Otherwise, returns true.
723 isDocumentWideEvent_: function() {
724 return this.document_.activeElement.nodeName.toLowerCase() !== 'input' ||
725 this.document_.activeElement.type.toLowerCase() !== 'text';
729 * @this {FileTransferController}
731 onCopy_: function(event) {
732 if (!this.isDocumentWideEvent_() ||
733 !this.canCopyOrDrag_()) {
736 event.preventDefault();
737 this.cutOrCopy_(event.clipboardData, 'copy');
738 this.notify_('selection-copied');
742 * @this {FileTransferController}
744 onBeforeCopy_: function(event) {
745 if (!this.isDocumentWideEvent_())
748 // queryCommandEnabled returns true if event.defaultPrevented is true.
749 if (this.canCopyOrDrag_())
750 event.preventDefault();
754 * @this {FileTransferController}
755 * @return {boolean} Returns true if all selected files are available to be
758 isAllSelectedFilesAvailable_: function() {
759 if (!this.currentDirectoryContentEntry)
761 var volumeInfo = this.volumeManager_.getVolumeInfo(
762 this.currentDirectoryContentEntry);
765 var isDriveOffline = this.volumeManager_.getDriveConnectionState().type ===
766 VolumeManagerCommon.DriveConnectionType.OFFLINE;
767 if (this.isOnDrive && isDriveOffline && !this.allDriveFilesAvailable)
773 * @this {FileTransferController}
774 * @return {boolean} Returns true if some files are selected and all the file
775 * on drive is available to be copied. Otherwise, returns false.
777 canCopyOrDrag_: function() {
778 return this.isAllSelectedFilesAvailable_() &&
779 this.selectedEntries_.length > 0;
783 * @this {FileTransferController}
785 onCut_: function(event) {
786 if (!this.isDocumentWideEvent_() ||
787 !this.canCutOrDrag_()) {
790 event.preventDefault();
791 this.cutOrCopy_(event.clipboardData, 'move');
792 this.notify_('selection-cut');
796 * @this {FileTransferController}
798 onBeforeCut_: function(event) {
799 if (!this.isDocumentWideEvent_())
801 // queryCommandEnabled returns true if event.defaultPrevented is true.
802 if (this.canCutOrDrag_())
803 event.preventDefault();
807 * @this {FileTransferController}
808 * @return {boolean} Returns true if the current directory is not read only.
810 canCutOrDrag_: function() {
811 return !this.readonly && this.selectedEntries_.length > 0;
815 * @this {FileTransferController}
817 onPaste_: function(event) {
818 // If the event has destDirectory property, paste files into the directory.
819 // This occurs when the command fires from menu item 'Paste into folder'.
820 var destination = event.destDirectory || this.currentDirectoryContentEntry;
822 // Need to update here since 'beforepaste' doesn't fire.
823 if (!this.isDocumentWideEvent_() ||
824 !this.canPasteOrDrop_(event.clipboardData, destination)) {
827 event.preventDefault();
828 var effect = this.paste(event.clipboardData, destination);
830 // On cut, we clear the clipboard after the file is pasted/moved so we don't
831 // try to move/delete the original file again.
832 if (effect === 'move') {
833 this.simulateCommand_('cut', function(event) {
834 event.preventDefault();
835 event.clipboardData.setData('fs/clear', '');
841 * @this {FileTransferController}
843 onBeforePaste_: function(event) {
844 if (!this.isDocumentWideEvent_())
846 // queryCommandEnabled returns true if event.defaultPrevented is true.
847 if (this.canPasteOrDrop_(event.clipboardData,
848 this.currentDirectoryContentEntry)) {
849 event.preventDefault();
854 * @this {FileTransferController}
855 * @param {DataTransfer} dataTransfer Data transfer object.
856 * @param {DirectoryEntry} destinationEntry Destination entry.
857 * @return {boolean} Returns true if items stored in {@code dataTransfer} can
858 * be pasted to {@code destinationEntry}. Otherwise, returns false.
860 canPasteOrDrop_: function(dataTransfer, destinationEntry) {
861 if (!destinationEntry)
863 var destinationLocationInfo =
864 this.volumeManager_.getLocationInfo(destinationEntry);
865 if (!destinationLocationInfo || destinationLocationInfo.isReadOnly)
867 if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') === -1)
868 return false; // Unsupported type of content.
870 // Copying between different sources requires all files to be available.
871 if (this.getSourceRootURL_(dataTransfer) !==
872 destinationLocationInfo.volumeInfo.fileSystem.root.toURL() &&
873 this.isMissingFileContents_(dataTransfer))
880 * Execute paste command.
882 * @this {FileTransferController}
883 * @return {boolean} Returns true, the paste is success. Otherwise, returns
886 queryPasteCommandEnabled: function() {
887 if (!this.isDocumentWideEvent_()) {
891 // HACK(serya): return this.document_.queryCommandEnabled('paste')
894 this.simulateCommand_('paste', function(event) {
895 result = this.canPasteOrDrop_(event.clipboardData,
896 this.currentDirectoryContentEntry);
902 * Allows to simulate commands to get access to clipboard.
904 * @this {FileTransferController}
905 * @param {string} command 'copy', 'cut' or 'paste'.
906 * @param {function} handler Event handler.
908 simulateCommand_: function(command, handler) {
909 var iframe = this.document_.querySelector('#command-dispatcher');
910 var doc = iframe.contentDocument;
911 doc.addEventListener(command, handler);
912 doc.execCommand(command);
913 doc.removeEventListener(command, handler);
917 * @this {FileTransferController}
919 onSelectionChanged_: function(event) {
920 var entries = this.selectedEntries_;
921 var files = this.selectedFileObjects_ = [];
922 this.preloadedThumbnailImageNode_ = null;
924 var fileEntries = [];
925 for (var i = 0; i < entries.length; i++) {
926 if (entries[i].isFile)
927 fileEntries.push(entries[i]);
929 var containsDirectory = fileEntries.length !== entries.length;
931 // File object must be prepeared in advance for clipboard operations
932 // (copy, paste and drag). DataTransfer object closes for write after
933 // returning control from that handlers so they may not have
934 // asynchronous operations.
935 if (!containsDirectory) {
936 for (var i = 0; i < fileEntries.length; i++) {
937 fileEntries[i].file(function(file) { files.push(file); });
941 if (entries.length === 1) {
942 // For single selection, the dragged element is created in advance,
943 // otherwise an image may not be loaded at the time the 'dragstart' event
945 this.preloadThumbnailImage_(entries[0]);
948 if (this.isOnDrive) {
949 this.allDriveFilesAvailable = false;
950 this.metadataCache_.get(entries, 'drive', function(props) {
951 // We consider directories not available offline for the purposes of
952 // file transfer since we cannot afford to recursive traversal.
953 this.allDriveFilesAvailable =
954 !containsDirectory &&
955 props.filter(function(p) {
956 return !p.availableOffline;
958 // |Copy| is the only menu item affected by allDriveFilesAvailable.
959 // It could be open right now, update its UI.
960 this.copyCommand_.disabled = !this.canCopyOrDrag_();
966 * Obains directory that is displaying now.
967 * @this {FileTransferController}
968 * @return {DirectoryEntry} Entry of directry that is displaying now.
970 get currentDirectoryContentEntry() {
971 return this.directoryModel_.getCurrentDirEntry();
975 * @this {FileTransferController}
976 * @return {boolean} True if the current directory is read only.
979 return this.directoryModel_.isReadOnly();
983 * @this {FileTransferController}
984 * @return {boolean} True if the current directory is on Drive.
987 var currentDir = this.directoryModel_.getCurrentDirEntry();
990 var locationInfo = this.volumeManager_.getLocationInfo(currentDir);
993 return locationInfo.isDriveBased;
997 * @this {FileTransferController}
999 notify_: function(eventName) {
1001 // Set timeout to avoid recursive events.
1002 setTimeout(function() {
1003 cr.dispatchSimpleEvent(self, eventName);
1008 * @this {FileTransferController}
1009 * @return {Array.<Entry>} Array of the selected entries.
1011 get selectedEntries_() {
1012 var list = this.directoryModel_.getFileList();
1013 var selectedIndexes = this.directoryModel_.getFileListSelection().
1015 var entries = selectedIndexes.map(function(index) {
1016 return list.item(index);
1019 // TODO(serya): Diagnostics for http://crbug/129642
1020 if (entries.indexOf(undefined) !== -1) {
1021 var index = entries.indexOf(undefined);
1022 entries = entries.filter(function(e) { return !!e; });
1023 console.error('Invalid selection found: list items: ', list.length,
1024 'wrong indexe value: ', selectedIndexes[index],
1025 'Stack trace: ', new Error().stack);
1031 * @param {Event} event Drag event.
1032 * @param {DirectoryEntry} destinationEntry Destination entry.
1033 * @this {FileTransferController}
1034 * @return {string} Returns the appropriate drop query type ('none', 'move'
1035 * or copy') to the current modifiers status and the destination.
1037 selectDropEffect_: function(event, destinationEntry) {
1038 if (!destinationEntry)
1040 var destinationLocationInfo =
1041 this.volumeManager_.getLocationInfo(destinationEntry);
1042 if (!destinationLocationInfo)
1044 if (destinationLocationInfo.isReadOnly)
1046 if (event.dataTransfer.effectAllowed === 'move')
1048 // TODO(mtomasz): Use volumeId instead of comparing roots, as soon as
1049 // volumeId gets unique.
1050 if (event.dataTransfer.effectAllowed === 'copyMove' &&
1051 this.getSourceRootURL_(event.dataTransfer) ===
1052 destinationLocationInfo.volumeInfo.fileSystem.root.toURL() &&
1056 if (event.dataTransfer.effectAllowed === 'copyMove' &&