Upstream version 10.39.225.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  * @param {ProgressCenter} progressCenter To notify starting copy operation.
25  * @constructor
26  */
27 function FileTransferController(doc,
28                                 fileOperationManager,
29                                 metadataCache,
30                                 directoryModel,
31                                 volumeManager,
32                                 multiProfileShareDialog,
33                                 progressCenter) {
34   this.document_ = doc;
35   this.fileOperationManager_ = fileOperationManager;
36   this.metadataCache_ = metadataCache;
37   this.directoryModel_ = directoryModel;
38   this.volumeManager_ = volumeManager;
39   this.multiProfileShareDialog_ = multiProfileShareDialog;
40   this.progressCenter_ = progressCenter;
41
42   this.directoryModel_.getFileList().addEventListener(
43       'change',
44       function(event) {
45         if (this.directoryModel_.getFileListSelection().
46             getIndexSelected(event.index)) {
47           this.onSelectionChanged_();
48         }
49       }.bind(this));
50   this.directoryModel_.getFileListSelection().addEventListener('change',
51       this.onSelectionChanged_.bind(this));
52
53   /**
54    * The array of pending task ID.
55    * @type {Array.<string>}
56    */
57   this.pendingTaskIds = [];
58
59   /**
60    * Promise to be fulfilled with the thumbnail image of selected file in drag
61    * operation. Used if only one element is selected.
62    * @type {Promise}
63    * @private
64    */
65   this.preloadedThumbnailImagePromise_ = null;
66
67   /**
68    * File objects for selected files.
69    *
70    * @type {Array.<File>}
71    * @private
72    */
73   this.selectedFileObjects_ = [];
74
75   /**
76    * Drag selector.
77    * @type {DragSelector}
78    * @private
79    */
80   this.dragSelector_ = new DragSelector();
81
82   /**
83    * Whether a user is touching the device or not.
84    * @type {boolean}
85    * @private
86    */
87   this.touching_ = false;
88
89   /**
90    * Task ID counter.
91    * @type {number}
92    * @private
93    */
94   this.taskIdCounter_ = 0;
95 }
96
97 /**
98  * Size of drag thumbnail for image files.
99  *
100  * @type {number}
101  * @const
102  * @private
103  */
104 FileTransferController.DRAG_THUMBNAIL_SIZE_ = 64;
105
106 FileTransferController.prototype = {
107   __proto__: cr.EventTarget.prototype,
108
109   /**
110    * @this {FileTransferController}
111    * @param {cr.ui.List} list Items in the list will be draggable.
112    */
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);
122   },
123
124   /**
125    * @this {FileTransferController}
126    * @param {cr.ui.List} list List itself and its directory items will could
127    *                          be drop target.
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).
131    */
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));
140   },
141
142   /**
143    * @this {FileTransferController}
144    * @param {DirectoryTree} tree Its sub items will could be drop target.
145    */
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));
151   },
152
153   /**
154    * Attach handlers of copy, cut and paste operations to the document.
155    *
156    * @this {FileTransferController}
157    */
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');
172   },
173
174   /**
175    * Write the current selection to system clipboard.
176    *
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.
181    */
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_());
196
197     for (var i = 0; i < this.selectedFileObjects_.length; i++) {
198       dataTransfer.items.add(this.selectedFileObjects_[i]);
199     }
200   },
201
202   /**
203    * @this {FileTransferController}
204    * @return {Object.<string, string>} Drag and drop global data object.
205    */
206   getDragAndDropGlobalData_: function() {
207     if (window[DRAG_AND_DROP_GLOBAL_DATA])
208       return window[DRAG_AND_DROP_GLOBAL_DATA];
209
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];
215     }
216     return null;
217   },
218
219   /**
220    * Extracts source root URL from the |dataTransfer| object.
221    *
222    * @this {FileTransferController}
223    * @param {DataTransfer} dataTransfer DataTransfer object from the event.
224    * @return {string} URL or an empty string (if unknown).
225    */
226   getSourceRootURL_: function(dataTransfer) {
227     var sourceRootURL = dataTransfer.getData('fs/sourceRootURL');
228     if (sourceRootURL)
229       return sourceRootURL;
230
231     // |dataTransfer| in protected mode.
232     var globalData = this.getDragAndDropGlobalData_();
233     if (globalData)
234       return globalData.sourceRootURL;
235
236     // Unknown source.
237     return '';
238   },
239
240   /**
241    * @this {FileTransferController}
242    * @param {DataTransfer} dataTransfer DataTransfer object from the event.
243    * @return {boolean} Returns true when missing some file contents.
244    */
245   isMissingFileContents_: function(dataTransfer) {
246     var data = dataTransfer.getData('fs/missingFileContents');
247     if (!data) {
248       // |dataTransfer| in protected mode.
249       var globalData = this.getDragAndDropGlobalData_();
250       if (globalData)
251         data = globalData.missingFileContents;
252     }
253     return data === 'true';
254   },
255
256   /**
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
261    *     share.
262    */
263   getMultiProfileShareEntries_: function(entries) {
264     // Utility function to concat arrays.
265     var concatArrays = function(arrays) {
266       return Array.prototype.concat.apply([], arrays);
267     };
268
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);
277     };
278
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.
283         // crbug.com/345527.
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;
290         });
291       });
292     };
293
294     // Check child entries.
295     var processDirectoryEntry = function(entry) {
296       return readEntries(entry.createReader());
297     };
298
299     // Read entries from DirectoryReader and call processEntries for the chunk
300     // of entries.
301     var readEntries = function(reader) {
302       return new Promise(reader.readEntries.bind(reader)).then(
303           function(entries) {
304             if (entries.length > 0) {
305               return Promise.all(
306                   [processEntries(entries), readEntries(reader)]).
307                   then(concatArrays);
308             } else {
309               return [];
310             }
311           },
312           function(error) {
313             console.warn(
314                 'Error happens while reading directory.', error);
315             return [];
316           });
317     }.bind(this);
318
319     // Filter entries that is owned by the current user, and call
320     // processEntries.
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);
324     }.bind(this)));
325   },
326
327   /**
328    * Queue up a file copy operation based on the current system clipboard.
329    *
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".
337    */
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
342     // work fine.
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;
350     var entries;
351     var failureUrls;
352     var taskId = this.fileOperationManager_.generateTaskId();
353
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();
359       item.id = taskId;
360       item.type = ProgressItemType.COPY;
361       if (result.entries.length === 1)
362         item.message = strf('COPY_FILE_NAME', result.entries[0].name);
363       else
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)
370         return;
371       return this.multiProfileShareDialog_.show(shareEntries.length > 1).
372           then(function(dialogResult) {
373             if (dialogResult === 'cancel')
374               return Promise.reject('ABORT');
375             // Do cross share.
376             // TODO(hirono): Make the loop cancellable.
377             var requestDriveShare = function(index) {
378               if (index >= shareEntries.length)
379                 return;
380               return new Promise(function(fulfill) {
381                 chrome.fileManagerPrivate.requestDriveShare(
382                     shareEntries[index].toURL(),
383                     dialogResult,
384                     function() {
385                       // TODO(hirono): Check chrome.runtime.lastError here.
386                       fulfill();
387                     });
388               }).then(requestDriveShare.bind(null, index + 1));
389             };
390             return requestDriveShare(0);
391           });
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);
397
398       // Publish events for failureUrls.
399       for (var i = 0; i < failureUrls.length; i++) {
400         var fileName =
401             decodeURIComponent(failureUrls[i].replace(/^.+\//, ''));
402         var event = new Event('source-not-found');
403         event.fileName = fileName;
404         event.progressType =
405             toMove ? ProgressItemType.MOVE : ProgressItemType.COPY;
406         this.dispatchEvent(event);
407       }
408     }.bind(this)).catch(function(error) {
409       if (error !== 'ABORT')
410         console.error(error.stack ? error.stack : error);
411     });
412     return toMove ? 'move' : 'copy';
413   },
414
415   /**
416    * Preloads an image thumbnail for the specified file entry.
417    *
418    * @this {FileTransferController}
419    * @param {Entry} entry Entry to preload a thumbnail for.
420    */
421   preloadThumbnailImage_: function(entry) {
422     var metadataPromise = new Promise(function(fulfill, reject) {
423       this.metadataCache_.getOne(
424           entry,
425           'thumbnail|filesystem',
426           function(metadata) {
427             if (metadata)
428               fulfill(metadata);
429             else
430               reject('Failed to fetch metadata.');
431           });
432     }.bind(this));
433
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) {
439           if (result)
440             fulfill(loader.getImage());
441         });
442       });
443     });
444
445     imagePromise.then(function(image) {
446       // Store the image so that we can obtain the image synchronously.
447       imagePromise.value = image;
448     }, function(error) {
449       console.error(error.stack || error);
450     });
451
452     this.preloadedThumbnailImagePromise_ = imagePromise;
453   },
454
455   /**
456    * Renders a drag-and-drop thumbnail.
457    *
458    * @this {FileTransferController}
459    * @return {HTMLElement} Element containing the thumbnail.
460    */
461   renderThumbnail_: function() {
462     var length = this.selectedEntries_.length;
463
464     var container = this.document_.querySelector('#drag-container');
465     var contents = this.document_.createElement('div');
466     contents.className = 'drag-contents';
467     container.appendChild(contents);
468
469     // Option 1. Multiple selection, render only a label.
470     if (length > 1) {
471       var label = this.document_.createElement('div');
472       label.className = 'label';
473       label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
474       contents.appendChild(label);
475       return container;
476     }
477
478     // Option 2. Thumbnail image available, then render it without
479     // a label.
480     if (this.preloadedThumbnailImagePromise_ &&
481         this.preloadedThumbnailImagePromise_.value) {
482       var thumbnailImage = this.preloadedThumbnailImagePromise_.value;
483
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_;
488
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);
494
495       var context = canvas.getContext('2d');
496       context.drawImage(thumbnailImage,
497                         (thumbnailImage.width - srcWidth) / 2,
498                         (thumbnailImage.height - srcHeight) / 2,
499                         srcWidth,
500                         srcHeight,
501                         0,
502                         0,
503                         canvas.width,
504                         canvas.height);
505       contents.classList.add('for-image');
506       contents.appendChild(canvas);
507       return container;
508     }
509
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);
520     return container;
521   },
522
523   /**
524    * @this {FileTransferController}
525    * @param {cr.ui.List} list Drop target list
526    * @param {Event} event A dragstart event of DOM.
527    */
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.
533       if (!this.touching_)
534         this.dragSelector_.startDragSelection(list, event);
535       return;
536     }
537
538     // Nothing selected.
539     if (!this.selectedEntries_.length) {
540       event.preventDefault();
541       return;
542     }
543
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');
552       } else {
553         this.cutOrCopy_(dt, 'move');
554       }
555     } else {
556       event.preventDefault();
557       return;
558     }
559
560     var dragThumbnail = this.renderThumbnail_();
561     dt.setDragImage(dragThumbnail, 0, 0);
562
563     window[DRAG_AND_DROP_GLOBAL_DATA] = {
564       sourceRootURL: dt.getData('fs/sourceRootURL'),
565       missingFileContents: dt.getData('fs/missingFileContents')
566     };
567   },
568
569   /**
570    * @this {FileTransferController}
571    * @param {cr.ui.List} list Drop target list.
572    * @param {Event} event A dragend event of DOM.
573    */
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;
578
579     var container = this.document_.querySelector('#drag-container');
580     container.textContent = '';
581     this.clearDropTarget_();
582     delete window[DRAG_AND_DROP_GLOBAL_DATA];
583   },
584
585   /**
586    * @this {FileTransferController}
587    * @param {boolean} onlyIntoDirectories True if the drag is only into
588    *     directories.
589    * @param {cr.ui.List} list Drop target list.
590    * @param {Event} event A dragover event of DOM.
591    */
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();
598   },
599
600   /**
601    * @this {FileTransferController}
602    * @param {cr.ui.List} list Drop target list.
603    * @param {Event} event A dragenter event of DOM.
604    */
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_)
611       return;
612
613     var entry = item && list.dataModel.item(item.listIndex);
614     if (entry)
615       this.setDropTarget_(item, event.dataTransfer, entry);
616     else
617       this.clearDropTarget_();
618   },
619
620   /**
621    * @this {FileTransferController}
622    * @param {DirectoryTree} tree Drop target tree.
623    * @param {Event} event A dragenter event of DOM.
624    */
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;
631     }
632
633     if (item === this.dropTarget_)
634       return;
635
636     var entry = item && item.entry;
637     if (entry) {
638       this.setDropTarget_(item, event.dataTransfer, entry);
639     } else {
640       this.clearDropTarget_();
641     }
642   },
643
644   /**
645    * @this {FileTransferController}
646    * @param {cr.ui.List} list Drop target list.
647    * @param {Event} event A dragleave event of DOM.
648    */
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;
659     }
660   },
661
662   /**
663    * @this {FileTransferController}
664    * @param {boolean} onlyIntoDirectories True if the drag is only into
665    *     directories.
666    * @param {Event} event A dragleave event of DOM.
667    */
668   onDrop_: function(onlyIntoDirectories, event) {
669     if (onlyIntoDirectories && !this.dropTarget_)
670       return;
671     var destinationEntry = this.destinationEntry_ ||
672                            this.currentDirectoryContentEntry;
673     if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry))
674       return;
675     event.preventDefault();
676     this.paste(event.dataTransfer, destinationEntry,
677                this.selectDropEffect_(event, destinationEntry));
678     this.clearDropTarget_();
679   },
680
681   /**
682    * Sets the drop target.
683    *
684    * @this {FileTransferController}
685    * @param {Element} domElement Target of the drop.
686    * @param {DataTransfer} dataTransfer Data transfer object.
687    * @param {DirectoryEntry} destinationEntry Destination entry.
688    */
689   setDropTarget_: function(domElement, dataTransfer, destinationEntry) {
690     if (this.dropTarget_ === domElement)
691       return;
692
693     // Remove the old drop target.
694     this.clearDropTarget_();
695
696     // Set the new drop target.
697     this.dropTarget_ = domElement;
698
699     if (!domElement ||
700         !destinationEntry.isDirectory ||
701         !this.canPasteOrDrop_(dataTransfer, destinationEntry)) {
702       return;
703     }
704
705     // Add accept class if the domElement can accept the drag.
706     domElement.classList.add('accepts');
707     this.destinationEntry_ = destinationEntry;
708
709     // Start timer changing the directory.
710     this.navigateTimer_ = setTimeout(function() {
711       if (domElement instanceof DirectoryItem)
712         // Do custom action.
713         (/** @type {DirectoryItem} */ domElement).doDropTargetAction();
714       this.directoryModel_.changeDirectoryEntry(destinationEntry);
715     }.bind(this), 2000);
716   },
717
718   /**
719    * Handles touch start.
720    */
721   onTouchStart_: function() {
722     this.touching_ = true;
723   },
724
725   /**
726    * Handles touch end.
727    */
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;
734   },
735
736   /**
737    * Clears the drop target.
738    * @this {FileTransferController}
739    */
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;
748     }
749   },
750
751   /**
752    * @this {FileTransferController}
753    * @return {boolean} Returns false if {@code <input type="text">} element is
754    *     currently active. Otherwise, returns true.
755    */
756   isDocumentWideEvent_: function() {
757     return this.document_.activeElement.nodeName.toLowerCase() !== 'input' ||
758         this.document_.activeElement.type.toLowerCase() !== 'text';
759   },
760
761   /**
762    * @this {FileTransferController}
763    */
764   onCopy_: function(event) {
765     if (!this.isDocumentWideEvent_() ||
766         !this.canCopyOrDrag_()) {
767       return;
768     }
769     event.preventDefault();
770     this.cutOrCopy_(event.clipboardData, 'copy');
771     this.notify_('selection-copied');
772   },
773
774   /**
775    * @this {FileTransferController}
776    */
777   onBeforeCopy_: function(event) {
778     if (!this.isDocumentWideEvent_())
779       return;
780
781     // queryCommandEnabled returns true if event.defaultPrevented is true.
782     if (this.canCopyOrDrag_())
783       event.preventDefault();
784   },
785
786   /**
787    * @this {FileTransferController}
788    * @return {boolean} Returns true if all selected files are available to be
789    *     copied.
790    */
791   isAllSelectedFilesAvailable_: function() {
792     if (!this.currentDirectoryContentEntry)
793       return false;
794     var volumeInfo = this.volumeManager_.getVolumeInfo(
795         this.currentDirectoryContentEntry);
796     if (!volumeInfo)
797       return false;
798     var isDriveOffline = this.volumeManager_.getDriveConnectionState().type ===
799         VolumeManagerCommon.DriveConnectionType.OFFLINE;
800     if (this.isOnDrive && isDriveOffline && !this.allDriveFilesAvailable)
801       return false;
802     return true;
803   },
804
805   /**
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.
809    */
810   canCopyOrDrag_: function() {
811     return this.isAllSelectedFilesAvailable_() &&
812         this.selectedEntries_.length > 0;
813   },
814
815   /**
816    * @this {FileTransferController}
817    */
818   onCut_: function(event) {
819     if (!this.isDocumentWideEvent_() ||
820         !this.canCutOrDrag_()) {
821       return;
822     }
823     event.preventDefault();
824     this.cutOrCopy_(event.clipboardData, 'move');
825     this.notify_('selection-cut');
826   },
827
828   /**
829    * @this {FileTransferController}
830    */
831   onBeforeCut_: function(event) {
832     if (!this.isDocumentWideEvent_())
833       return;
834     // queryCommandEnabled returns true if event.defaultPrevented is true.
835     if (this.canCutOrDrag_())
836       event.preventDefault();
837   },
838
839   /**
840    * @this {FileTransferController}
841    * @return {boolean} Returns true if the current directory is not read only.
842    */
843   canCutOrDrag_: function() {
844     return !this.readonly && this.selectedEntries_.length > 0;
845   },
846
847   /**
848    * @this {FileTransferController}
849    */
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;
854
855     // Need to update here since 'beforepaste' doesn't fire.
856     if (!this.isDocumentWideEvent_() ||
857         !this.canPasteOrDrop_(event.clipboardData, destination)) {
858       return;
859     }
860     event.preventDefault();
861     var effect = this.paste(event.clipboardData, destination);
862
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', '');
869       });
870     }
871   },
872
873   /**
874    * @this {FileTransferController}
875    */
876   onBeforePaste_: function(event) {
877     if (!this.isDocumentWideEvent_())
878       return;
879     // queryCommandEnabled returns true if event.defaultPrevented is true.
880     if (this.canPasteOrDrop_(event.clipboardData,
881                              this.currentDirectoryContentEntry)) {
882       event.preventDefault();
883     }
884   },
885
886   /**
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.
892    */
893   canPasteOrDrop_: function(dataTransfer, destinationEntry) {
894     if (!destinationEntry)
895       return false;
896     var destinationLocationInfo =
897         this.volumeManager_.getLocationInfo(destinationEntry);
898     if (!destinationLocationInfo || destinationLocationInfo.isReadOnly)
899       return false;
900     if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') === -1)
901       return false;  // Unsupported type of content.
902
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))
907       return false;
908
909     return true;
910   },
911
912   /**
913    * Execute paste command.
914    *
915    * @this {FileTransferController}
916    * @return {boolean}  Returns true, the paste is success. Otherwise, returns
917    *     false.
918    */
919   queryPasteCommandEnabled: function() {
920     if (!this.isDocumentWideEvent_()) {
921       return false;
922     }
923
924     // HACK(serya): return this.document_.queryCommandEnabled('paste')
925     // should be used.
926     var result;
927     this.simulateCommand_('paste', function(event) {
928       result = this.canPasteOrDrop_(event.clipboardData,
929                                     this.currentDirectoryContentEntry);
930     }.bind(this));
931     return result;
932   },
933
934   /**
935    * Allows to simulate commands to get access to clipboard.
936    *
937    * @this {FileTransferController}
938    * @param {string} command 'copy', 'cut' or 'paste'.
939    * @param {function} handler Event handler.
940    */
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);
947   },
948
949   /**
950    * @this {FileTransferController}
951    */
952   onSelectionChanged_: function(event) {
953     var entries = this.selectedEntries_;
954     var files = this.selectedFileObjects_ = [];
955     this.preloadedThumbnailImagePromise_ = null;
956
957     var fileEntries = [];
958     for (var i = 0; i < entries.length; i++) {
959       if (entries[i].isFile)
960         fileEntries.push(entries[i]);
961     }
962     var containsDirectory = fileEntries.length !== entries.length;
963
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); });
971       }
972     }
973
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
977       // comes.
978       this.preloadThumbnailImage_(entries[0]);
979     }
980
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;
990             }).length === 0;
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_();
994       }.bind(this));
995     }
996   },
997
998   /**
999    * Obains directory that is displaying now.
1000    * @this {FileTransferController}
1001    * @return {DirectoryEntry} Entry of directry that is displaying now.
1002    */
1003   get currentDirectoryContentEntry() {
1004     return this.directoryModel_.getCurrentDirEntry();
1005   },
1006
1007   /**
1008    * @this {FileTransferController}
1009    * @return {boolean} True if the current directory is read only.
1010    */
1011   get readonly() {
1012     return this.directoryModel_.isReadOnly();
1013   },
1014
1015   /**
1016    * @this {FileTransferController}
1017    * @return {boolean} True if the current directory is on Drive.
1018    */
1019   get isOnDrive() {
1020     var currentDir = this.directoryModel_.getCurrentDirEntry();
1021     if (!currentDir)
1022       return false;
1023     var locationInfo = this.volumeManager_.getLocationInfo(currentDir);
1024     if (!locationInfo)
1025       return false;
1026     return locationInfo.isDriveBased;
1027   },
1028
1029   /**
1030    * @this {FileTransferController}
1031    */
1032   notify_: function(eventName) {
1033     var self = this;
1034     // Set timeout to avoid recursive events.
1035     setTimeout(function() {
1036       cr.dispatchSimpleEvent(self, eventName);
1037     }, 0);
1038   },
1039
1040   /**
1041    * @this {FileTransferController}
1042    * @return {Array.<Entry>} Array of the selected entries.
1043    */
1044   get selectedEntries_() {
1045     var list = this.directoryModel_.getFileList();
1046     var selectedIndexes = this.directoryModel_.getFileListSelection().
1047         selectedIndexes;
1048     var entries = selectedIndexes.map(function(index) {
1049       return list.item(index);
1050     });
1051
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);
1059     }
1060     return entries;
1061   },
1062
1063   /**
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.
1069    */
1070   selectDropEffect_: function(event, destinationEntry) {
1071     if (!destinationEntry)
1072       return 'none';
1073     var destinationLocationInfo =
1074         this.volumeManager_.getLocationInfo(destinationEntry);
1075     if (!destinationLocationInfo)
1076       return 'none';
1077     if (destinationLocationInfo.isReadOnly)
1078       return 'none';
1079     if (util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'move')) {
1080       if (!util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'copy'))
1081         return 'move';
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() &&
1086           !event.ctrlKey) {
1087         return 'move';
1088       }
1089       if (event.shiftKey) {
1090         return 'move';
1091       }
1092     }
1093     return 'copy';
1094   },
1095 };