• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
13var 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 */
26function 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 */
87FileTransferController.DRAG_THUMBNAIL_SIZE_ = 64;
88
89FileTransferController.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 DirectoryItem)) {
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};
1112