Upstream version 7.36.149.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    * DOM element to represent selected file in drag operation. Used if only
51    * one element is selected.
52    * @type {HTMLElement}
53    * @private
54    */
55   this.preloadedThumbnailImageNode_ = 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 FileTransferController.prototype = {
81   __proto__: cr.EventTarget.prototype,
82
83   /**
84    * @this {FileTransferController}
85    * @param {cr.ui.List} list Items in the list will be draggable.
86    */
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);
96   },
97
98   /**
99    * @this {FileTransferController}
100    * @param {cr.ui.List} list List itself and its directory items will could
101    *                          be drop target.
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).
105    */
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));
114   },
115
116   /**
117    * @this {FileTransferController}
118    * @param {DirectoryTree} tree Its sub items will could be drop target.
119    */
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));
125   },
126
127   /**
128    * @this {FileTransferController}
129    * @param {NavigationList} tree Its sub items will could be drop target.
130    */
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 */));
139   },
140
141   /**
142    * Attach handlers of copy, cut and paste operations to the document.
143    *
144    * @this {FileTransferController}
145    */
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');
160   },
161
162   /**
163    * Write the current selection to system clipboard.
164    *
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').
169    */
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_());
184
185     for (var i = 0; i < this.selectedFileObjects_.length; i++) {
186       dataTransfer.items.add(this.selectedFileObjects_[i]);
187     }
188   },
189
190   /**
191    * @this {FileTransferController}
192    * @return {Object.<string, string>} Drag and drop global data object.
193    */
194   getDragAndDropGlobalData_: function() {
195     if (window[DRAG_AND_DROP_GLOBAL_DATA])
196       return window[DRAG_AND_DROP_GLOBAL_DATA];
197
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];
203     }
204     return null;
205   },
206
207   /**
208    * Extracts source root URL from the |dataTransfer| object.
209    *
210    * @this {FileTransferController}
211    * @param {DataTransfer} dataTransfer DataTransfer object from the event.
212    * @return {string} URL or an empty string (if unknown).
213    */
214   getSourceRootURL_: function(dataTransfer) {
215     var sourceRootURL = dataTransfer.getData('fs/sourceRootURL');
216     if (sourceRootURL)
217       return sourceRootURL;
218
219     // |dataTransfer| in protected mode.
220     var globalData = this.getDragAndDropGlobalData_();
221     if (globalData)
222       return globalData.sourceRootURL;
223
224     // Unknown source.
225     return '';
226   },
227
228   /**
229    * @this {FileTransferController}
230    * @param {DataTransfer} dataTransfer DataTransfer object from the event.
231    * @return {boolean} Returns true when missing some file contents.
232    */
233   isMissingFileContents_: function(dataTransfer) {
234     var data = dataTransfer.getData('fs/missingFileContents');
235     if (!data) {
236       // |dataTransfer| in protected mode.
237       var globalData = this.getDragAndDropGlobalData_();
238       if (globalData)
239         data = globalData.missingFileContents;
240     }
241     return data === 'true';
242   },
243
244   /**
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
249    *     share.
250    */
251   getMultiProfileShareEntries_: function(entries) {
252     // Utility function to concat arrays.
253     var concatArrays = function(arrays) {
254       return Array.prototype.concat.apply([], arrays);
255     };
256
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);
265     };
266
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);
272       }).
273       then(function(metadatas) {
274         return entries.filter(function(entry, i) {
275           var metadata = metadatas[i];
276           return metadata && metadata.isHosted && !metadata.sharedWithMe;
277         });
278       });
279     };
280
281     // Check child entries.
282     var processDirectoryEntry = function(entry) {
283       return readEntries(entry.createReader());
284     };
285
286     // Read entries from DirectoryReader and call processEntries for the chunk
287     // of entries.
288     var readEntries = function(reader) {
289       return new Promise(reader.readEntries.bind(reader)).then(
290           function(entries) {
291             if (entries.length > 0) {
292               return Promise.all(
293                   [processEntries(entries), readEntries(reader)]).
294                   then(concatArrays);
295             } else {
296               return [];
297             }
298           },
299           function(error) {
300             console.warn(
301                 'Error happens while reading directory.', error);
302             return [];
303           });
304     }.bind(this);
305
306     // Filter entries that is owned by the current user, and call
307     // processEntries.
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);
311     }.bind(this)));
312   },
313
314   /**
315    * Queue up a file copy operation based on the current system clipboard.
316    *
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".
324    */
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
329     // work fine.
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;
336     var entries;
337     var failureUrls;
338
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);
345     }.bind(this)).
346     then(function(shareEntries) {
347       if (shareEntries.length === 0)
348         return;
349       return this.multiProfileShareDialog_.show(shareEntries.length > 1).
350           then(function(dialogResult) {
351             if (dialogResult === 'cancel')
352               return Promise.reject('ABORT');
353             // Do cross share.
354             // TODO(hirono): Make the loop cancellable.
355             var requestDriveShare = function(index) {
356               if (index >= shareEntries.length)
357                 return;
358               return new Promise(function(fulfill) {
359                 chrome.fileBrowserPrivate.requestDriveShare(
360                     shareEntries[index].toURL(),
361                     dialogResult,
362                     function() {
363                       // TODO(hirono): Check chrome.runtime.lastError here.
364                       fulfill();
365                     });
366               }).then(requestDriveShare.bind(null, index + 1));
367             };
368             return requestDriveShare(0);
369           });
370     }.bind(this)).
371     then(function() {
372       // Start the pasting operation.
373       this.fileOperationManager_.paste(
374           entries, destinationEntry, toMove);
375
376       // Publish events for failureUrls.
377       for (var i = 0; i < failureUrls.length; i++) {
378         var fileName =
379             decodeURIComponent(failureUrls[i].replace(/^.+\//, ''));
380         var event = new Event('source-not-found');
381         event.fileName = fileName;
382         event.progressType =
383             toMove ? ProgressItemType.MOVE : ProgressItemType.COPY;
384         this.dispatchEvent(event);
385       }
386     }.bind(this)).
387     catch(function(error) {
388       if (error !== 'ABORT')
389         console.error(error.stack ? error.stack : error);
390     });
391     return toMove ? 'move' : 'copy';
392   },
393
394   /**
395    * Preloads an image thumbnail for the specified file entry.
396    *
397    * @this {FileTransferController}
398    * @param {Entry} entry Entry to preload a thumbnail for.
399    */
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(
406         entry,
407         metadataTypes,
408         function(metadata) {
409           new ThumbnailLoader(entry,
410                               ThumbnailLoader.LoaderType.IMAGE,
411                               metadata).
412               load(thumbnailContainer,
413                    ThumbnailLoader.FillMode.FILL);
414         }.bind(this));
415   },
416
417   /**
418    * Renders a drag-and-drop thumbnail.
419    *
420    * @this {FileTransferController}
421    * @return {HTMLElement} Element containing the thumbnail.
422    */
423   renderThumbnail_: function() {
424     var length = this.selectedEntries_.length;
425
426     var container = this.document_.querySelector('#drag-container');
427     var contents = this.document_.createElement('div');
428     contents.className = 'drag-contents';
429     container.appendChild(contents);
430
431     var thumbnailImage;
432     if (this.preloadedThumbnailImageNode_)
433       thumbnailImage = this.preloadedThumbnailImageNode_.querySelector('img');
434
435     // Option 1. Multiple selection, render only a label.
436     if (length > 1) {
437       var label = this.document_.createElement('div');
438       label.className = 'label';
439       label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
440       contents.appendChild(label);
441       return container;
442     }
443
444     // Option 2. Thumbnail image available, then render it without
445     // a label.
446     if (thumbnailImage) {
447       thumbnailImage.classList.add('drag-thumbnail');
448       contents.classList.add('for-image');
449       contents.appendChild(this.preloadedThumbnailImageNode_);
450       return container;
451     }
452
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);
463     return container;
464   },
465
466   /**
467    * @this {FileTransferController}
468    * @param {cr.ui.List} list Drop target list
469    * @param {Event} event A dragstart event of DOM.
470    */
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();
475       return;
476     }
477
478     // Check if a drag selection should be initiated or not.
479     if (list.shouldStartDragSelection(event)) {
480       this.dragSelector_.startDragSelection(list, event);
481       return;
482     }
483
484     // Nothing selected.
485     if (!this.selectedEntries_.length) {
486       event.preventDefault();
487       return;
488     }
489
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');
498       } else {
499         this.cutOrCopy_(dt, 'move');
500       }
501     } else {
502       event.preventDefault();
503       return;
504     }
505
506     var dragThumbnail = this.renderThumbnail_();
507     dt.setDragImage(dragThumbnail, 1000, 1000);
508
509     window[DRAG_AND_DROP_GLOBAL_DATA] = {
510       sourceRootURL: dt.getData('fs/sourceRootURL'),
511       missingFileContents: dt.getData('fs/missingFileContents'),
512     };
513   },
514
515   /**
516    * @this {FileTransferController}
517    * @param {cr.ui.List} list Drop target list.
518    * @param {Event} event A dragend event of DOM.
519    */
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];
525   },
526
527   /**
528    * @this {FileTransferController}
529    * @param {boolean} onlyIntoDirectories True if the drag is only into
530    *     directories.
531    * @param {cr.ui.List} list Drop target list.
532    * @param {Event} event A dragover event of DOM.
533    */
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();
540   },
541
542   /**
543    * @this {FileTransferController}
544    * @param {cr.ui.List} list Drop target list.
545    * @param {Event} event A dragenter event of DOM.
546    */
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_)
553       return;
554
555     var entry = item && list.dataModel.item(item.listIndex);
556     if (entry)
557       this.setDropTarget_(item, event.dataTransfer, entry);
558     else
559       this.clearDropTarget_();
560   },
561
562   /**
563    * @this {FileTransferController}
564    * @param {DirectoryTree} tree Drop target tree.
565    * @param {Event} event A dragenter event of DOM.
566    */
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;
573     }
574
575     if (item === this.dropTarget_)
576       return;
577
578     var entry = item && item.entry;
579     if (entry) {
580       this.setDropTarget_(item, event.dataTransfer, entry);
581     } else {
582       this.clearDropTarget_();
583     }
584   },
585
586   /**
587    * @this {FileTransferController}
588    * @param {NavigationList} list Drop target list.
589    * @param {Event} event A dragenter event of DOM.
590    */
591   onDragEnterVolumesList_: function(list, event) {
592     event.preventDefault();  // Required to prevent the cursor flicker.
593
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_)
598       return;
599
600     var modelItem = item && list.dataModel.item(item.listIndex);
601     if (modelItem && modelItem.isShortcut) {
602       this.setDropTarget_(item, event.dataTransfer, modelItem.entry);
603       return;
604     }
605     if (modelItem && modelItem.isVolume && modelItem.volumeInfo.displayRoot) {
606       this.setDropTarget_(
607           item, event.dataTransfer, modelItem.volumeInfo.displayRoot);
608       return;
609     }
610
611     this.clearDropTarget_();
612   },
613
614   /**
615    * @this {FileTransferController}
616    * @param {cr.ui.List} list Drop target list.
617    * @param {Event} event A dragleave event of DOM.
618    */
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;
629     }
630   },
631
632   /**
633    * @this {FileTransferController}
634    * @param {boolean} onlyIntoDirectories True if the drag is only into
635    *     directories.
636    * @param {Event} event A dragleave event of DOM.
637    */
638   onDrop_: function(onlyIntoDirectories, event) {
639     if (onlyIntoDirectories && !this.dropTarget_)
640       return;
641     var destinationEntry = this.destinationEntry_ ||
642                            this.currentDirectoryContentEntry;
643     if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry))
644       return;
645     event.preventDefault();
646     this.paste(event.dataTransfer, destinationEntry,
647                this.selectDropEffect_(event, destinationEntry));
648     this.clearDropTarget_();
649   },
650
651   /**
652    * Sets the drop target.
653    *
654    * @this {FileTransferController}
655    * @param {Element} domElement Target of the drop.
656    * @param {DataTransfer} dataTransfer Data transfer object.
657    * @param {DirectoryEntry} destinationEntry Destination entry.
658    */
659   setDropTarget_: function(domElement, dataTransfer, destinationEntry) {
660     if (this.dropTarget_ === domElement)
661       return;
662
663     // Remove the old drop target.
664     this.clearDropTarget_();
665
666     // Set the new drop target.
667     this.dropTarget_ = domElement;
668
669     if (!domElement ||
670         !destinationEntry.isDirectory ||
671         !this.canPasteOrDrop_(dataTransfer, destinationEntry)) {
672       return;
673     }
674
675     // Add accept class if the domElement can accept the drag.
676     domElement.classList.add('accepts');
677     this.destinationEntry_ = destinationEntry;
678
679     // Start timer changing the directory.
680     this.navigateTimer_ = setTimeout(function() {
681       if (domElement instanceof DirectoryItem)
682         // Do custom action.
683         (/** @type {DirectoryItem} */ domElement).doDropTargetAction();
684       this.directoryModel_.changeDirectoryEntry(destinationEntry);
685     }.bind(this), 2000);
686   },
687
688   /**
689    * Handles touch start.
690    */
691   onTouchStart_: function() {
692     this.touching_ = true;
693   },
694
695   /**
696    * Handles touch end.
697    */
698   onTouchEnd_: function(event) {
699     if (event.touches.length === 0)
700       this.touching_ = false;
701   },
702
703   /**
704    * Clears the drop target.
705    * @this {FileTransferController}
706    */
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;
715     }
716   },
717
718   /**
719    * @this {FileTransferController}
720    * @return {boolean} Returns false if {@code <input type="text">} element is
721    *     currently active. Otherwise, returns true.
722    */
723   isDocumentWideEvent_: function() {
724     return this.document_.activeElement.nodeName.toLowerCase() !== 'input' ||
725         this.document_.activeElement.type.toLowerCase() !== 'text';
726   },
727
728   /**
729    * @this {FileTransferController}
730    */
731   onCopy_: function(event) {
732     if (!this.isDocumentWideEvent_() ||
733         !this.canCopyOrDrag_()) {
734       return;
735     }
736     event.preventDefault();
737     this.cutOrCopy_(event.clipboardData, 'copy');
738     this.notify_('selection-copied');
739   },
740
741   /**
742    * @this {FileTransferController}
743    */
744   onBeforeCopy_: function(event) {
745     if (!this.isDocumentWideEvent_())
746       return;
747
748     // queryCommandEnabled returns true if event.defaultPrevented is true.
749     if (this.canCopyOrDrag_())
750       event.preventDefault();
751   },
752
753   /**
754    * @this {FileTransferController}
755    * @return {boolean} Returns true if all selected files are available to be
756    *     copied.
757    */
758   isAllSelectedFilesAvailable_: function() {
759     if (!this.currentDirectoryContentEntry)
760       return false;
761     var volumeInfo = this.volumeManager_.getVolumeInfo(
762         this.currentDirectoryContentEntry);
763     if (!volumeInfo)
764       return false;
765     var isDriveOffline = this.volumeManager_.getDriveConnectionState().type ===
766         VolumeManagerCommon.DriveConnectionType.OFFLINE;
767     if (this.isOnDrive && isDriveOffline && !this.allDriveFilesAvailable)
768       return false;
769     return true;
770   },
771
772   /**
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.
776    */
777   canCopyOrDrag_: function() {
778     return this.isAllSelectedFilesAvailable_() &&
779         this.selectedEntries_.length > 0;
780   },
781
782   /**
783    * @this {FileTransferController}
784    */
785   onCut_: function(event) {
786     if (!this.isDocumentWideEvent_() ||
787         !this.canCutOrDrag_()) {
788       return;
789     }
790     event.preventDefault();
791     this.cutOrCopy_(event.clipboardData, 'move');
792     this.notify_('selection-cut');
793   },
794
795   /**
796    * @this {FileTransferController}
797    */
798   onBeforeCut_: function(event) {
799     if (!this.isDocumentWideEvent_())
800       return;
801     // queryCommandEnabled returns true if event.defaultPrevented is true.
802     if (this.canCutOrDrag_())
803       event.preventDefault();
804   },
805
806   /**
807    * @this {FileTransferController}
808    * @return {boolean} Returns true if the current directory is not read only.
809    */
810   canCutOrDrag_: function() {
811     return !this.readonly && this.selectedEntries_.length > 0;
812   },
813
814   /**
815    * @this {FileTransferController}
816    */
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;
821
822     // Need to update here since 'beforepaste' doesn't fire.
823     if (!this.isDocumentWideEvent_() ||
824         !this.canPasteOrDrop_(event.clipboardData, destination)) {
825       return;
826     }
827     event.preventDefault();
828     var effect = this.paste(event.clipboardData, destination);
829
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', '');
836       });
837     }
838   },
839
840   /**
841    * @this {FileTransferController}
842    */
843   onBeforePaste_: function(event) {
844     if (!this.isDocumentWideEvent_())
845       return;
846     // queryCommandEnabled returns true if event.defaultPrevented is true.
847     if (this.canPasteOrDrop_(event.clipboardData,
848                              this.currentDirectoryContentEntry)) {
849       event.preventDefault();
850     }
851   },
852
853   /**
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.
859    */
860   canPasteOrDrop_: function(dataTransfer, destinationEntry) {
861     if (!destinationEntry)
862       return false;
863     var destinationLocationInfo =
864         this.volumeManager_.getLocationInfo(destinationEntry);
865     if (!destinationLocationInfo || destinationLocationInfo.isReadOnly)
866       return false;
867     if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') === -1)
868       return false;  // Unsupported type of content.
869
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))
874       return false;
875
876     return true;
877   },
878
879   /**
880    * Execute paste command.
881    *
882    * @this {FileTransferController}
883    * @return {boolean}  Returns true, the paste is success. Otherwise, returns
884    *     false.
885    */
886   queryPasteCommandEnabled: function() {
887     if (!this.isDocumentWideEvent_()) {
888       return false;
889     }
890
891     // HACK(serya): return this.document_.queryCommandEnabled('paste')
892     // should be used.
893     var result;
894     this.simulateCommand_('paste', function(event) {
895       result = this.canPasteOrDrop_(event.clipboardData,
896                                     this.currentDirectoryContentEntry);
897     }.bind(this));
898     return result;
899   },
900
901   /**
902    * Allows to simulate commands to get access to clipboard.
903    *
904    * @this {FileTransferController}
905    * @param {string} command 'copy', 'cut' or 'paste'.
906    * @param {function} handler Event handler.
907    */
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);
914   },
915
916   /**
917    * @this {FileTransferController}
918    */
919   onSelectionChanged_: function(event) {
920     var entries = this.selectedEntries_;
921     var files = this.selectedFileObjects_ = [];
922     this.preloadedThumbnailImageNode_ = null;
923
924     var fileEntries = [];
925     for (var i = 0; i < entries.length; i++) {
926       if (entries[i].isFile)
927         fileEntries.push(entries[i]);
928     }
929     var containsDirectory = fileEntries.length !== entries.length;
930
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); });
938       }
939     }
940
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
944       // comes.
945       this.preloadThumbnailImage_(entries[0]);
946     }
947
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;
957             }).length === 0;
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_();
961       }.bind(this));
962     }
963   },
964
965   /**
966    * Obains directory that is displaying now.
967    * @this {FileTransferController}
968    * @return {DirectoryEntry} Entry of directry that is displaying now.
969    */
970   get currentDirectoryContentEntry() {
971     return this.directoryModel_.getCurrentDirEntry();
972   },
973
974   /**
975    * @this {FileTransferController}
976    * @return {boolean} True if the current directory is read only.
977    */
978   get readonly() {
979     return this.directoryModel_.isReadOnly();
980   },
981
982   /**
983    * @this {FileTransferController}
984    * @return {boolean} True if the current directory is on Drive.
985    */
986   get isOnDrive() {
987     var currentDir = this.directoryModel_.getCurrentDirEntry();
988     if (!currentDir)
989       return false;
990     var locationInfo = this.volumeManager_.getLocationInfo(currentDir);
991     if (!locationInfo)
992       return false;
993     return locationInfo.isDriveBased;
994   },
995
996   /**
997    * @this {FileTransferController}
998    */
999   notify_: function(eventName) {
1000     var self = this;
1001     // Set timeout to avoid recursive events.
1002     setTimeout(function() {
1003       cr.dispatchSimpleEvent(self, eventName);
1004     }, 0);
1005   },
1006
1007   /**
1008    * @this {FileTransferController}
1009    * @return {Array.<Entry>} Array of the selected entries.
1010    */
1011   get selectedEntries_() {
1012     var list = this.directoryModel_.getFileList();
1013     var selectedIndexes = this.directoryModel_.getFileListSelection().
1014         selectedIndexes;
1015     var entries = selectedIndexes.map(function(index) {
1016       return list.item(index);
1017     });
1018
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);
1026     }
1027     return entries;
1028   },
1029
1030   /**
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.
1036    */
1037   selectDropEffect_: function(event, destinationEntry) {
1038     if (!destinationEntry)
1039       return 'none';
1040     var destinationLocationInfo =
1041         this.volumeManager_.getLocationInfo(destinationEntry);
1042     if (!destinationLocationInfo)
1043       return 'none';
1044     if (destinationLocationInfo.isReadOnly)
1045       return 'none';
1046     if (event.dataTransfer.effectAllowed === 'move')
1047       return '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() &&
1053         !event.ctrlKey) {
1054       return 'move';
1055     }
1056     if (event.dataTransfer.effectAllowed === 'copyMove' &&
1057         event.shiftKey) {
1058       return 'move';
1059     }
1060     return 'copy';
1061   },
1062 };