Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / file_transfer_controller.js
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 'use strict';
6
7 /**
8  * Global (placed in the window object) variable name to hold internal
9  * file dragging information. Needed to show visual feedback while dragging
10  * since DataTransfer object is in protected state. Reachable from other
11  * file manager instances.
12  */
13 var DRAG_AND_DROP_GLOBAL_DATA = '__drag_and_drop_global_data';
14
15 /**
16  * @param {HTMLDocument} doc Owning document.
17  * @param {FileOperationManager} fileOperationManager File operation manager
18  *     instance.
19  * @param {MetadataCache} metadataCache Metadata cache service.
20  * @param {DirectoryModel} directoryModel Directory model instance.
21  * @param {VolumeManagerWrapper} volumeManager Volume manager instance.
22  * @param {MultiProfileShareDialog} multiProfileShareDialog Share dialog to be
23  *     used to share files from another profile.
24  * @constructor
25  */
26 function FileTransferController(doc,
27                                 fileOperationManager,
28                                 metadataCache,
29                                 directoryModel,
30                                 volumeManager,
31                                 multiProfileShareDialog) {
32   this.document_ = doc;
33   this.fileOperationManager_ = fileOperationManager;
34   this.metadataCache_ = metadataCache;
35   this.directoryModel_ = directoryModel;
36   this.volumeManager_ = volumeManager;
37   this.multiProfileShareDialog_ = multiProfileShareDialog;
38
39   this.directoryModel_.getFileList().addEventListener(
40       'change', function(event) {
41     if (this.directoryModel_.getFileListSelection().
42         getIndexSelected(event.index)) {
43       this.onSelectionChanged_();
44     }
45   }.bind(this));
46   this.directoryModel_.getFileListSelection().addEventListener('change',
47       this.onSelectionChanged_.bind(this));
48
49   /**
50    * Promise to be fulfilled with the thumbnail image of selected file in drag
51    * operation. Used if only one element is selected.
52    * @type {Promise}
53    * @private
54    */
55   this.preloadedThumbnailImagePromise_ = null;
56
57   /**
58    * File objects for selected files.
59    *
60    * @type {Array.<File>}
61    * @private
62    */
63   this.selectedFileObjects_ = [];
64
65   /**
66    * Drag selector.
67    * @type {DragSelector}
68    * @private
69    */
70   this.dragSelector_ = new DragSelector();
71
72   /**
73    * Whether a user is touching the device or not.
74    * @type {boolean}
75    * @private
76    */
77   this.touching_ = false;
78 }
79
80 /**
81  * Size of drag thumbnail for image files.
82  *
83  * @type {number}
84  * @const
85  * @private
86  */
87 FileTransferController.DRAG_THUMBNAIL_SIZE_ = 64;
88
89 FileTransferController.prototype = {
90   __proto__: cr.EventTarget.prototype,
91
92   /**
93    * @this {FileTransferController}
94    * @param {cr.ui.List} list Items in the list will be draggable.
95    */
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);
105   },
106
107   /**
108    * @this {FileTransferController}
109    * @param {cr.ui.List} list List itself and its directory items will could
110    *                          be drop target.
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).
114    */
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));
123   },
124
125   /**
126    * @this {FileTransferController}
127    * @param {DirectoryTree} tree Its sub items will could be drop target.
128    */
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));
134   },
135
136   /**
137    * @this {FileTransferController}
138    * @param {NavigationList} tree Its sub items will could be drop target.
139    */
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 */));
148   },
149
150   /**
151    * Attach handlers of copy, cut and paste operations to the document.
152    *
153    * @this {FileTransferController}
154    */
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');
169   },
170
171   /**
172    * Write the current selection to system clipboard.
173    *
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.
178    */
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_());
193
194     for (var i = 0; i < this.selectedFileObjects_.length; i++) {
195       dataTransfer.items.add(this.selectedFileObjects_[i]);
196     }
197   },
198
199   /**
200    * @this {FileTransferController}
201    * @return {Object.<string, string>} Drag and drop global data object.
202    */
203   getDragAndDropGlobalData_: function() {
204     if (window[DRAG_AND_DROP_GLOBAL_DATA])
205       return window[DRAG_AND_DROP_GLOBAL_DATA];
206
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];
212     }
213     return null;
214   },
215
216   /**
217    * Extracts source root URL from the |dataTransfer| object.
218    *
219    * @this {FileTransferController}
220    * @param {DataTransfer} dataTransfer DataTransfer object from the event.
221    * @return {string} URL or an empty string (if unknown).
222    */
223   getSourceRootURL_: function(dataTransfer) {
224     var sourceRootURL = dataTransfer.getData('fs/sourceRootURL');
225     if (sourceRootURL)
226       return sourceRootURL;
227
228     // |dataTransfer| in protected mode.
229     var globalData = this.getDragAndDropGlobalData_();
230     if (globalData)
231       return globalData.sourceRootURL;
232
233     // Unknown source.
234     return '';
235   },
236
237   /**
238    * @this {FileTransferController}
239    * @param {DataTransfer} dataTransfer DataTransfer object from the event.
240    * @return {boolean} Returns true when missing some file contents.
241    */
242   isMissingFileContents_: function(dataTransfer) {
243     var data = dataTransfer.getData('fs/missingFileContents');
244     if (!data) {
245       // |dataTransfer| in protected mode.
246       var globalData = this.getDragAndDropGlobalData_();
247       if (globalData)
248         data = globalData.missingFileContents;
249     }
250     return data === 'true';
251   },
252
253   /**
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
258    *     share.
259    */
260   getMultiProfileShareEntries_: function(entries) {
261     // Utility function to concat arrays.
262     var concatArrays = function(arrays) {
263       return Array.prototype.concat.apply([], arrays);
264     };
265
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);
274     };
275
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);
281       }).
282       then(function(metadatas) {
283         return entries.filter(function(entry, i) {
284           var metadata = metadatas[i];
285           return metadata && metadata.isHosted && !metadata.sharedWithMe;
286         });
287       });
288     };
289
290     // Check child entries.
291     var processDirectoryEntry = function(entry) {
292       return readEntries(entry.createReader());
293     };
294
295     // Read entries from DirectoryReader and call processEntries for the chunk
296     // of entries.
297     var readEntries = function(reader) {
298       return new Promise(reader.readEntries.bind(reader)).then(
299           function(entries) {
300             if (entries.length > 0) {
301               return Promise.all(
302                   [processEntries(entries), readEntries(reader)]).
303                   then(concatArrays);
304             } else {
305               return [];
306             }
307           },
308           function(error) {
309             console.warn(
310                 'Error happens while reading directory.', error);
311             return [];
312           });
313     }.bind(this);
314
315     // Filter entries that is owned by the current user, and call
316     // processEntries.
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);
320     }.bind(this)));
321   },
322
323   /**
324    * Queue up a file copy operation based on the current system clipboard.
325    *
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".
333    */
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
338     // work fine.
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;
346     var entries;
347     var failureUrls;
348
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);
355     }.bind(this)).
356     then(function(shareEntries) {
357       if (shareEntries.length === 0)
358         return;
359       return this.multiProfileShareDialog_.show(shareEntries.length > 1).
360           then(function(dialogResult) {
361             if (dialogResult === 'cancel')
362               return Promise.reject('ABORT');
363             // Do cross share.
364             // TODO(hirono): Make the loop cancellable.
365             var requestDriveShare = function(index) {
366               if (index >= shareEntries.length)
367                 return;
368               return new Promise(function(fulfill) {
369                 chrome.fileBrowserPrivate.requestDriveShare(
370                     shareEntries[index].toURL(),
371                     dialogResult,
372                     function() {
373                       // TODO(hirono): Check chrome.runtime.lastError here.
374                       fulfill();
375                     });
376               }).then(requestDriveShare.bind(null, index + 1));
377             };
378             return requestDriveShare(0);
379           });
380     }.bind(this)).
381     then(function() {
382       // Start the pasting operation.
383       this.fileOperationManager_.paste(
384           entries, destinationEntry, toMove);
385
386       // Publish events for failureUrls.
387       for (var i = 0; i < failureUrls.length; i++) {
388         var fileName =
389             decodeURIComponent(failureUrls[i].replace(/^.+\//, ''));
390         var event = new Event('source-not-found');
391         event.fileName = fileName;
392         event.progressType =
393             toMove ? ProgressItemType.MOVE : ProgressItemType.COPY;
394         this.dispatchEvent(event);
395       }
396     }.bind(this)).
397     catch(function(error) {
398       if (error !== 'ABORT')
399         console.error(error.stack ? error.stack : error);
400     });
401     return toMove ? 'move' : 'copy';
402   },
403
404   /**
405    * Preloads an image thumbnail for the specified file entry.
406    *
407    * @this {FileTransferController}
408    * @param {Entry} entry Entry to preload a thumbnail for.
409    */
410   preloadThumbnailImage_: function(entry) {
411     var metadataPromise = new Promise(function(fulfill, reject) {
412       this.metadataCache_.getOne(entry,
413                                  'thumbnail|filesystem',
414                                  function(metadata) {
415         if (metadata)
416           fulfill(metadata);
417         else
418           reject('Failed to fetch metadata.');
419       });
420     }.bind(this));
421
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) {
427           if (result)
428             fulfill(loader.getImage());
429         });
430       });
431     });
432
433     imagePromise.then(function(image) {
434       // Store the image so that we can obtain the image synchronously.
435       imagePromise.value = image;
436     }, function(error) {
437       console.error(error.stack || error);
438     });
439
440     this.preloadedThumbnailImagePromise_ = imagePromise;
441   },
442
443   /**
444    * Renders a drag-and-drop thumbnail.
445    *
446    * @this {FileTransferController}
447    * @return {HTMLElement} Element containing the thumbnail.
448    */
449   renderThumbnail_: function() {
450     var length = this.selectedEntries_.length;
451
452     var container = this.document_.querySelector('#drag-container');
453     var contents = this.document_.createElement('div');
454     contents.className = 'drag-contents';
455     container.appendChild(contents);
456
457     // Option 1. Multiple selection, render only a label.
458     if (length > 1) {
459       var label = this.document_.createElement('div');
460       label.className = 'label';
461       label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
462       contents.appendChild(label);
463       return container;
464     }
465
466     // Option 2. Thumbnail image available, then render it without
467     // a label.
468     if (this.preloadedThumbnailImagePromise_ &&
469         this.preloadedThumbnailImagePromise_.value) {
470       var thumbnailImage = this.preloadedThumbnailImagePromise_.value;
471
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_;
476
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);
482
483       var context = canvas.getContext('2d');
484       context.drawImage(thumbnailImage,
485                         (thumbnailImage.width - srcWidth) / 2,
486                         (thumbnailImage.height - srcHeight) / 2,
487                         srcWidth,
488                         srcHeight,
489                         0,
490                         0,
491                         canvas.width,
492                         canvas.height);
493       contents.classList.add('for-image');
494       contents.appendChild(canvas);
495       return container;
496     }
497
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);
508     return container;
509   },
510
511   /**
512    * @this {FileTransferController}
513    * @param {cr.ui.List} list Drop target list
514    * @param {Event} event A dragstart event of DOM.
515    */
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.
521       if (!this.touching_)
522         this.dragSelector_.startDragSelection(list, event);
523       return;
524     }
525
526     // Nothing selected.
527     if (!this.selectedEntries_.length) {
528       event.preventDefault();
529       return;
530     }
531
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');
540       } else {
541         this.cutOrCopy_(dt, 'move');
542       }
543     } else {
544       event.preventDefault();
545       return;
546     }
547
548     var dragThumbnail = this.renderThumbnail_();
549     dt.setDragImage(dragThumbnail, 0, 0);
550
551     window[DRAG_AND_DROP_GLOBAL_DATA] = {
552       sourceRootURL: dt.getData('fs/sourceRootURL'),
553       missingFileContents: dt.getData('fs/missingFileContents'),
554     };
555   },
556
557   /**
558    * @this {FileTransferController}
559    * @param {cr.ui.List} list Drop target list.
560    * @param {Event} event A dragend event of DOM.
561    */
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;
566
567     var container = this.document_.querySelector('#drag-container');
568     container.textContent = '';
569     this.clearDropTarget_();
570     delete window[DRAG_AND_DROP_GLOBAL_DATA];
571   },
572
573   /**
574    * @this {FileTransferController}
575    * @param {boolean} onlyIntoDirectories True if the drag is only into
576    *     directories.
577    * @param {cr.ui.List} list Drop target list.
578    * @param {Event} event A dragover event of DOM.
579    */
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();
586   },
587
588   /**
589    * @this {FileTransferController}
590    * @param {cr.ui.List} list Drop target list.
591    * @param {Event} event A dragenter event of DOM.
592    */
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_)
599       return;
600
601     var entry = item && list.dataModel.item(item.listIndex);
602     if (entry)
603       this.setDropTarget_(item, event.dataTransfer, entry);
604     else
605       this.clearDropTarget_();
606   },
607
608   /**
609    * @this {FileTransferController}
610    * @param {DirectoryTree} tree Drop target tree.
611    * @param {Event} event A dragenter event of DOM.
612    */
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;
619     }
620
621     if (item === this.dropTarget_)
622       return;
623
624     var entry = item && item.entry;
625     if (entry) {
626       this.setDropTarget_(item, event.dataTransfer, entry);
627     } else {
628       this.clearDropTarget_();
629     }
630   },
631
632   /**
633    * @this {FileTransferController}
634    * @param {NavigationList} list Drop target list.
635    * @param {Event} event A dragenter event of DOM.
636    */
637   onDragEnterVolumesList_: function(list, event) {
638     event.preventDefault();  // Required to prevent the cursor flicker.
639
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_)
644       return;
645
646     var modelItem = item && list.dataModel.item(item.listIndex);
647     if (modelItem && modelItem.isShortcut) {
648       this.setDropTarget_(item, event.dataTransfer, modelItem.entry);
649       return;
650     }
651     if (modelItem && modelItem.isVolume && modelItem.volumeInfo.displayRoot) {
652       this.setDropTarget_(
653           item, event.dataTransfer, modelItem.volumeInfo.displayRoot);
654       return;
655     }
656
657     this.clearDropTarget_();
658   },
659
660   /**
661    * @this {FileTransferController}
662    * @param {cr.ui.List} list Drop target list.
663    * @param {Event} event A dragleave event of DOM.
664    */
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;
675     }
676   },
677
678   /**
679    * @this {FileTransferController}
680    * @param {boolean} onlyIntoDirectories True if the drag is only into
681    *     directories.
682    * @param {Event} event A dragleave event of DOM.
683    */
684   onDrop_: function(onlyIntoDirectories, event) {
685     if (onlyIntoDirectories && !this.dropTarget_)
686       return;
687     var destinationEntry = this.destinationEntry_ ||
688                            this.currentDirectoryContentEntry;
689     if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry))
690       return;
691     event.preventDefault();
692     this.paste(event.dataTransfer, destinationEntry,
693                this.selectDropEffect_(event, destinationEntry));
694     this.clearDropTarget_();
695   },
696
697   /**
698    * Sets the drop target.
699    *
700    * @this {FileTransferController}
701    * @param {Element} domElement Target of the drop.
702    * @param {DataTransfer} dataTransfer Data transfer object.
703    * @param {DirectoryEntry} destinationEntry Destination entry.
704    */
705   setDropTarget_: function(domElement, dataTransfer, destinationEntry) {
706     if (this.dropTarget_ === domElement)
707       return;
708
709     // Remove the old drop target.
710     this.clearDropTarget_();
711
712     // Set the new drop target.
713     this.dropTarget_ = domElement;
714
715     if (!domElement ||
716         !destinationEntry.isDirectory ||
717         !this.canPasteOrDrop_(dataTransfer, destinationEntry)) {
718       return;
719     }
720
721     // Add accept class if the domElement can accept the drag.
722     domElement.classList.add('accepts');
723     this.destinationEntry_ = destinationEntry;
724
725     // Start timer changing the directory.
726     this.navigateTimer_ = setTimeout(function() {
727       if (domElement instanceof DirectoryItem)
728         // Do custom action.
729         (/** @type {DirectoryItem} */ domElement).doDropTargetAction();
730       this.directoryModel_.changeDirectoryEntry(destinationEntry);
731     }.bind(this), 2000);
732   },
733
734   /**
735    * Handles touch start.
736    */
737   onTouchStart_: function() {
738     this.touching_ = true;
739   },
740
741   /**
742    * Handles touch end.
743    */
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;
750   },
751
752   /**
753    * Clears the drop target.
754    * @this {FileTransferController}
755    */
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;
764     }
765   },
766
767   /**
768    * @this {FileTransferController}
769    * @return {boolean} Returns false if {@code <input type="text">} element is
770    *     currently active. Otherwise, returns true.
771    */
772   isDocumentWideEvent_: function() {
773     return this.document_.activeElement.nodeName.toLowerCase() !== 'input' ||
774         this.document_.activeElement.type.toLowerCase() !== 'text';
775   },
776
777   /**
778    * @this {FileTransferController}
779    */
780   onCopy_: function(event) {
781     if (!this.isDocumentWideEvent_() ||
782         !this.canCopyOrDrag_()) {
783       return;
784     }
785     event.preventDefault();
786     this.cutOrCopy_(event.clipboardData, 'copy');
787     this.notify_('selection-copied');
788   },
789
790   /**
791    * @this {FileTransferController}
792    */
793   onBeforeCopy_: function(event) {
794     if (!this.isDocumentWideEvent_())
795       return;
796
797     // queryCommandEnabled returns true if event.defaultPrevented is true.
798     if (this.canCopyOrDrag_())
799       event.preventDefault();
800   },
801
802   /**
803    * @this {FileTransferController}
804    * @return {boolean} Returns true if all selected files are available to be
805    *     copied.
806    */
807   isAllSelectedFilesAvailable_: function() {
808     if (!this.currentDirectoryContentEntry)
809       return false;
810     var volumeInfo = this.volumeManager_.getVolumeInfo(
811         this.currentDirectoryContentEntry);
812     if (!volumeInfo)
813       return false;
814     var isDriveOffline = this.volumeManager_.getDriveConnectionState().type ===
815         VolumeManagerCommon.DriveConnectionType.OFFLINE;
816     if (this.isOnDrive && isDriveOffline && !this.allDriveFilesAvailable)
817       return false;
818     return true;
819   },
820
821   /**
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.
825    */
826   canCopyOrDrag_: function() {
827     return this.isAllSelectedFilesAvailable_() &&
828         this.selectedEntries_.length > 0;
829   },
830
831   /**
832    * @this {FileTransferController}
833    */
834   onCut_: function(event) {
835     if (!this.isDocumentWideEvent_() ||
836         !this.canCutOrDrag_()) {
837       return;
838     }
839     event.preventDefault();
840     this.cutOrCopy_(event.clipboardData, 'move');
841     this.notify_('selection-cut');
842   },
843
844   /**
845    * @this {FileTransferController}
846    */
847   onBeforeCut_: function(event) {
848     if (!this.isDocumentWideEvent_())
849       return;
850     // queryCommandEnabled returns true if event.defaultPrevented is true.
851     if (this.canCutOrDrag_())
852       event.preventDefault();
853   },
854
855   /**
856    * @this {FileTransferController}
857    * @return {boolean} Returns true if the current directory is not read only.
858    */
859   canCutOrDrag_: function() {
860     return !this.readonly && this.selectedEntries_.length > 0;
861   },
862
863   /**
864    * @this {FileTransferController}
865    */
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;
870
871     // Need to update here since 'beforepaste' doesn't fire.
872     if (!this.isDocumentWideEvent_() ||
873         !this.canPasteOrDrop_(event.clipboardData, destination)) {
874       return;
875     }
876     event.preventDefault();
877     var effect = this.paste(event.clipboardData, destination);
878
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', '');
885       });
886     }
887   },
888
889   /**
890    * @this {FileTransferController}
891    */
892   onBeforePaste_: function(event) {
893     if (!this.isDocumentWideEvent_())
894       return;
895     // queryCommandEnabled returns true if event.defaultPrevented is true.
896     if (this.canPasteOrDrop_(event.clipboardData,
897                              this.currentDirectoryContentEntry)) {
898       event.preventDefault();
899     }
900   },
901
902   /**
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.
908    */
909   canPasteOrDrop_: function(dataTransfer, destinationEntry) {
910     if (!destinationEntry)
911       return false;
912     var destinationLocationInfo =
913         this.volumeManager_.getLocationInfo(destinationEntry);
914     if (!destinationLocationInfo || destinationLocationInfo.isReadOnly)
915       return false;
916     if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') === -1)
917       return false;  // Unsupported type of content.
918
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))
923       return false;
924
925     return true;
926   },
927
928   /**
929    * Execute paste command.
930    *
931    * @this {FileTransferController}
932    * @return {boolean}  Returns true, the paste is success. Otherwise, returns
933    *     false.
934    */
935   queryPasteCommandEnabled: function() {
936     if (!this.isDocumentWideEvent_()) {
937       return false;
938     }
939
940     // HACK(serya): return this.document_.queryCommandEnabled('paste')
941     // should be used.
942     var result;
943     this.simulateCommand_('paste', function(event) {
944       result = this.canPasteOrDrop_(event.clipboardData,
945                                     this.currentDirectoryContentEntry);
946     }.bind(this));
947     return result;
948   },
949
950   /**
951    * Allows to simulate commands to get access to clipboard.
952    *
953    * @this {FileTransferController}
954    * @param {string} command 'copy', 'cut' or 'paste'.
955    * @param {function} handler Event handler.
956    */
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);
963   },
964
965   /**
966    * @this {FileTransferController}
967    */
968   onSelectionChanged_: function(event) {
969     var entries = this.selectedEntries_;
970     var files = this.selectedFileObjects_ = [];
971     this.preloadedThumbnailImagePromise_ = null;
972
973     var fileEntries = [];
974     for (var i = 0; i < entries.length; i++) {
975       if (entries[i].isFile)
976         fileEntries.push(entries[i]);
977     }
978     var containsDirectory = fileEntries.length !== entries.length;
979
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); });
987       }
988     }
989
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
993       // comes.
994       this.preloadThumbnailImage_(entries[0]);
995     }
996
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;
1006             }).length === 0;
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_();
1010       }.bind(this));
1011     }
1012   },
1013
1014   /**
1015    * Obains directory that is displaying now.
1016    * @this {FileTransferController}
1017    * @return {DirectoryEntry} Entry of directry that is displaying now.
1018    */
1019   get currentDirectoryContentEntry() {
1020     return this.directoryModel_.getCurrentDirEntry();
1021   },
1022
1023   /**
1024    * @this {FileTransferController}
1025    * @return {boolean} True if the current directory is read only.
1026    */
1027   get readonly() {
1028     return this.directoryModel_.isReadOnly();
1029   },
1030
1031   /**
1032    * @this {FileTransferController}
1033    * @return {boolean} True if the current directory is on Drive.
1034    */
1035   get isOnDrive() {
1036     var currentDir = this.directoryModel_.getCurrentDirEntry();
1037     if (!currentDir)
1038       return false;
1039     var locationInfo = this.volumeManager_.getLocationInfo(currentDir);
1040     if (!locationInfo)
1041       return false;
1042     return locationInfo.isDriveBased;
1043   },
1044
1045   /**
1046    * @this {FileTransferController}
1047    */
1048   notify_: function(eventName) {
1049     var self = this;
1050     // Set timeout to avoid recursive events.
1051     setTimeout(function() {
1052       cr.dispatchSimpleEvent(self, eventName);
1053     }, 0);
1054   },
1055
1056   /**
1057    * @this {FileTransferController}
1058    * @return {Array.<Entry>} Array of the selected entries.
1059    */
1060   get selectedEntries_() {
1061     var list = this.directoryModel_.getFileList();
1062     var selectedIndexes = this.directoryModel_.getFileListSelection().
1063         selectedIndexes;
1064     var entries = selectedIndexes.map(function(index) {
1065       return list.item(index);
1066     });
1067
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);
1075     }
1076     return entries;
1077   },
1078
1079   /**
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.
1085    */
1086   selectDropEffect_: function(event, destinationEntry) {
1087     if (!destinationEntry)
1088       return 'none';
1089     var destinationLocationInfo =
1090         this.volumeManager_.getLocationInfo(destinationEntry);
1091     if (!destinationLocationInfo)
1092       return 'none';
1093     if (destinationLocationInfo.isReadOnly)
1094       return 'none';
1095     if (util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'move')) {
1096       if (!util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'copy'))
1097         return 'move';
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() &&
1102           !event.ctrlKey) {
1103         return 'move';
1104       }
1105       if (event.shiftKey) {
1106         return 'move';
1107       }
1108     }
1109     return 'copy';
1110   },
1111 };