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 * @constructor
22 */
23function FileTransferController(doc,
24                                fileOperationManager,
25                                metadataCache,
26                                directoryModel) {
27  this.document_ = doc;
28  this.fileOperationManager_ = fileOperationManager;
29  this.metadataCache_ = metadataCache;
30  this.directoryModel_ = directoryModel;
31
32  this.directoryModel_.getFileListSelection().addEventListener('change',
33      this.onSelectionChanged_.bind(this));
34
35  /**
36   * DOM element to represent selected file in drag operation. Used if only
37   * one element is selected.
38   * @type {HTMLElement}
39   * @private
40   */
41  this.preloadedThumbnailImageNode_ = null;
42
43  /**
44   * File objects for selected files.
45   *
46   * @type {Array.<File>}
47   * @private
48   */
49  this.selectedFileObjects_ = [];
50
51  /**
52   * Drag selector.
53   * @type {DragSelector}
54   * @private
55   */
56  this.dragSelector_ = new DragSelector();
57
58  /**
59   * Whether a user is touching the device or not.
60   * @type {boolean}
61   * @private
62   */
63  this.touching_ = false;
64}
65
66FileTransferController.prototype = {
67  __proto__: cr.EventTarget.prototype,
68
69  /**
70   * @this {FileTransferController}
71   * @param {cr.ui.List} list Items in the list will be draggable.
72   */
73  attachDragSource: function(list) {
74    list.style.webkitUserDrag = 'element';
75    list.addEventListener('dragstart', this.onDragStart_.bind(this, list));
76    list.addEventListener('dragend', this.onDragEnd_.bind(this, list));
77    list.addEventListener('touchstart', this.onTouchStart_.bind(this));
78    list.addEventListener('touchend', this.onTouchEnd_.bind(this));
79  },
80
81  /**
82   * @this {FileTransferController}
83   * @param {cr.ui.List} list List itself and its directory items will could
84   *                          be drop target.
85   * @param {boolean=} opt_onlyIntoDirectories If true only directory list
86   *     items could be drop targets. Otherwise any other place of the list
87   *     accetps files (putting it into the current directory).
88   */
89  attachFileListDropTarget: function(list, opt_onlyIntoDirectories) {
90    list.addEventListener('dragover', this.onDragOver_.bind(this,
91        !!opt_onlyIntoDirectories, list));
92    list.addEventListener('dragenter',
93        this.onDragEnterFileList_.bind(this, list));
94    list.addEventListener('dragleave', this.onDragLeave_.bind(this, list));
95    list.addEventListener('drop',
96        this.onDrop_.bind(this, !!opt_onlyIntoDirectories));
97  },
98
99  /**
100   * @this {FileTransferController}
101   * @param {DirectoryTree} tree Its sub items will could be drop target.
102   */
103  attachTreeDropTarget: function(tree) {
104    tree.addEventListener('dragover', this.onDragOver_.bind(this, true, tree));
105    tree.addEventListener('dragenter', this.onDragEnterTree_.bind(this, tree));
106    tree.addEventListener('dragleave', this.onDragLeave_.bind(this, tree));
107    tree.addEventListener('drop', this.onDrop_.bind(this, true));
108  },
109
110  /**
111   * @this {FileTransferController}
112   * @param {NavigationList} tree Its sub items will could be drop target.
113   */
114  attachNavigationListDropTarget: function(list) {
115    list.addEventListener('dragover',
116        this.onDragOver_.bind(this, true /* onlyIntoDirectories */, list));
117    list.addEventListener('dragenter',
118        this.onDragEnterVolumesList_.bind(this, list));
119    list.addEventListener('dragleave', this.onDragLeave_.bind(this, list));
120    list.addEventListener('drop',
121        this.onDrop_.bind(this, true /* onlyIntoDirectories */));
122  },
123
124  /**
125   * Attach handlers of copy, cut and paste operations to the document.
126   *
127   * @this {FileTransferController}
128   */
129  attachCopyPasteHandlers: function() {
130    this.document_.addEventListener('beforecopy',
131                                    this.onBeforeCopy_.bind(this));
132    this.document_.addEventListener('copy',
133                                    this.onCopy_.bind(this));
134    this.document_.addEventListener('beforecut',
135                                    this.onBeforeCut_.bind(this));
136    this.document_.addEventListener('cut',
137                                    this.onCut_.bind(this));
138    this.document_.addEventListener('beforepaste',
139                                    this.onBeforePaste_.bind(this));
140    this.document_.addEventListener('paste',
141                                    this.onPaste_.bind(this));
142    this.copyCommand_ = this.document_.querySelector('command#copy');
143  },
144
145  /**
146   * Write the current selection to system clipboard.
147   *
148   * @this {FileTransferController}
149   * @param {DataTransfer} dataTransfer DataTransfer from the event.
150   * @param {string} effectAllowed Value must be valid for the
151   *     |dataTransfer.effectAllowed| property ('move', 'copy', 'copyMove').
152   */
153  cutOrCopy_: function(dataTransfer, effectAllowed) {
154    // Tag to check it's filemanager data.
155    dataTransfer.setData('fs/tag', 'filemanager-data');
156    dataTransfer.setData('fs/sourceRoot',
157                         this.directoryModel_.getCurrentRootPath());
158    var sourcePaths =
159        this.selectedEntries_.map(function(e) { return e.fullPath; });
160    dataTransfer.setData('fs/sources', sourcePaths.join('\n'));
161    dataTransfer.effectAllowed = effectAllowed;
162    dataTransfer.setData('fs/effectallowed', effectAllowed);
163
164    for (var i = 0; i < this.selectedFileObjects_.length; i++) {
165      dataTransfer.items.add(this.selectedFileObjects_[i]);
166    }
167  },
168
169  /**
170   * Extracts source root from the |dataTransfer| object.
171   *
172   * @this {FileTransferController}
173   * @param {DataTransfer} dataTransfer DataTransfer object from the event.
174   * @return {string} Path or empty string (if unknown).
175   */
176  getSourceRoot_: function(dataTransfer) {
177    var sourceRoot = dataTransfer.getData('fs/sourceRoot');
178    if (sourceRoot)
179      return sourceRoot;
180
181    // |dataTransfer| in protected mode.
182    if (window[DRAG_AND_DROP_GLOBAL_DATA])
183      return window[DRAG_AND_DROP_GLOBAL_DATA].sourceRoot;
184
185    // Dragging from other tabs/windows.
186    var views = chrome && chrome.extension ? chrome.extension.getViews() : [];
187    for (var i = 0; i < views.length; i++) {
188      if (views[i][DRAG_AND_DROP_GLOBAL_DATA])
189        return views[i][DRAG_AND_DROP_GLOBAL_DATA].sourceRoot;
190    }
191
192    // Unknown source.
193    return '';
194  },
195
196  /**
197   * Queue up a file copy operation based on the current system clipboard.
198   *
199   * @this {FileTransferController}
200   * @param {DataTransfer} dataTransfer System data transfer object.
201   * @param {string=} opt_destinationPath Paste destination.
202   * @param {string=} opt_effect Desired drop/paste effect. Could be
203   *     'move'|'copy' (default is copy). Ignored if conflicts with
204   *     |dataTransfer.effectAllowed|.
205   * @return {string} Either "copy" or "move".
206   */
207  paste: function(dataTransfer, opt_destinationPath, opt_effect) {
208    var sourcePaths = (dataTransfer.getData('fs/sources') || '').split('\n');
209    var destinationPath = opt_destinationPath ||
210                          this.currentDirectoryContentPath;
211    // effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers
212    // work fine.
213    var effectAllowed = dataTransfer.effectAllowed != 'uninitialized' ?
214        dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed');
215    var toMove = effectAllowed == 'move' ||
216        (effectAllowed == 'copyMove' && opt_effect == 'move');
217
218    // Start the pasting operation.
219    this.fileOperationManager_.paste(sourcePaths, destinationPath, toMove);
220    return toMove ? 'move' : 'copy';
221  },
222
223  /**
224   * Preloads an image thumbnail for the specified file entry.
225   *
226   * @this {FileTransferController}
227   * @param {Entry} entry Entry to preload a thumbnail for.
228   */
229  preloadThumbnailImage_: function(entry) {
230    var metadataTypes = 'thumbnail|filesystem';
231    var thumbnailContainer = this.document_.createElement('div');
232    this.preloadedThumbnailImageNode_ = thumbnailContainer;
233    this.preloadedThumbnailImageNode_.className = 'img-container';
234    this.metadataCache_.get(
235        entry,
236        metadataTypes,
237        function(metadata) {
238          new ThumbnailLoader(entry.toURL(),
239                              ThumbnailLoader.LoaderType.IMAGE,
240                              metadata).
241              load(thumbnailContainer,
242                   ThumbnailLoader.FillMode.FILL);
243        }.bind(this));
244  },
245
246  /**
247   * Renders a drag-and-drop thumbnail.
248   *
249   * @this {FileTransferController}
250   * @return {HTMLElement} Element containing the thumbnail.
251   */
252  renderThumbnail_: function() {
253    var length = this.selectedEntries_.length;
254
255    var container = this.document_.querySelector('#drag-container');
256    var contents = this.document_.createElement('div');
257    contents.className = 'drag-contents';
258    container.appendChild(contents);
259
260    var thumbnailImage;
261    if (this.preloadedThumbnailImageNode_)
262      thumbnailImage = this.preloadedThumbnailImageNode_.querySelector('img');
263
264    // Option 1. Multiple selection, render only a label.
265    if (length > 1) {
266      var label = this.document_.createElement('div');
267      label.className = 'label';
268      label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
269      contents.appendChild(label);
270      return container;
271    }
272
273    // Option 2. Thumbnail image available, then render it without
274    // a label.
275    if (thumbnailImage) {
276      thumbnailImage.classList.add('drag-thumbnail');
277      contents.classList.add('for-image');
278      contents.appendChild(this.preloadedThumbnailImageNode_);
279      return container;
280    }
281
282    // Option 3. Thumbnail not available. Render an icon and a label.
283    var entry = this.selectedEntries_[0];
284    var icon = this.document_.createElement('div');
285    icon.className = 'detail-icon';
286    icon.setAttribute('file-type-icon', FileType.getIcon(entry));
287    contents.appendChild(icon);
288    var label = this.document_.createElement('div');
289    label.className = 'label';
290    label.textContent = entry.name;
291    contents.appendChild(label);
292    return container;
293  },
294
295  /**
296   * @this {FileTransferController}
297   * @param {cr.ui.List} list Drop target list
298   * @param {Event} event A dragstart event of DOM.
299   */
300  onDragStart_: function(list, event) {
301    // If a user is touching, Files.app does not receive drag operations.
302    if (this.touching_) {
303      event.preventDefault();
304      return;
305    }
306
307    // Check if a drag selection should be initiated or not.
308    if (list.shouldStartDragSelection(event)) {
309      this.dragSelector_.startDragSelection(list, event);
310      return;
311    }
312
313    // Nothing selected.
314    if (!this.selectedEntries_.length) {
315      event.preventDefault();
316      return;
317    }
318
319    var dt = event.dataTransfer;
320
321    if (this.canCopyOrDrag_(dt)) {
322      if (this.canCutOrDrag_(dt))
323        this.cutOrCopy_(dt, 'copyMove');
324      else
325        this.cutOrCopy_(dt, 'copy');
326    } else {
327      event.preventDefault();
328      return;
329    }
330
331    var dragThumbnail = this.renderThumbnail_();
332    dt.setDragImage(dragThumbnail, 1000, 1000);
333
334    window[DRAG_AND_DROP_GLOBAL_DATA] = {
335      sourceRoot: this.directoryModel_.getCurrentRootPath()
336    };
337  },
338
339  /**
340   * @this {FileTransferController}
341   * @param {cr.ui.List} list Drop target list.
342   * @param {Event} event A dragend event of DOM.
343   */
344  onDragEnd_: function(list, event) {
345    var container = this.document_.querySelector('#drag-container');
346    container.textContent = '';
347    this.clearDropTarget_();
348    delete window[DRAG_AND_DROP_GLOBAL_DATA];
349  },
350
351  /**
352   * @this {FileTransferController}
353   * @param {boolean} onlyIntoDirectories True if the drag is only into
354   *     directories.
355   * @param {cr.ui.List} list Drop target list.
356   * @param {Event} event A dragover event of DOM.
357   */
358  onDragOver_: function(onlyIntoDirectories, list, event) {
359    event.preventDefault();
360    var path = this.destinationPath_ ||
361        (!onlyIntoDirectories && this.currentDirectoryContentPath);
362    event.dataTransfer.dropEffect = this.selectDropEffect_(event, path);
363    event.preventDefault();
364  },
365
366  /**
367   * @this {FileTransferController}
368   * @param {cr.ui.List} list Drop target list.
369   * @param {Event} event A dragenter event of DOM.
370   */
371  onDragEnterFileList_: function(list, event) {
372    event.preventDefault();  // Required to prevent the cursor flicker.
373    this.lastEnteredTarget_ = event.target;
374    var item = list.getListItemAncestor(event.target);
375    item = item && list.isItem(item) ? item : null;
376    if (item == this.dropTarget_)
377      return;
378
379    var entry = item && list.dataModel.item(item.listIndex);
380    if (entry) {
381      this.setDropTarget_(item, entry.isDirectory, event.dataTransfer,
382          entry.fullPath);
383    } else {
384      this.clearDropTarget_();
385    }
386  },
387
388  /**
389   * @this {FileTransferController}
390   * @param {DirectoryTree} tree Drop target tree.
391   * @param {Event} event A dragenter event of DOM.
392   */
393  onDragEnterTree_: function(tree, event) {
394    event.preventDefault();  // Required to prevent the cursor flicker.
395    this.lastEnteredTarget_ = event.target;
396    var item = event.target;
397    while (item && !(item instanceof DirectoryItem)) {
398      item = item.parentNode;
399    }
400
401    if (item == this.dropTarget_)
402      return;
403
404    var entry = item && item.entry;
405    if (entry) {
406      this.setDropTarget_(item, entry.isDirectory, event.dataTransfer,
407          entry.fullPath);
408    } else {
409      this.clearDropTarget_();
410    }
411  },
412
413  /**
414   * @this {FileTransferController}
415   * @param {NavigationList} list Drop target list.
416   * @param {Event} event A dragenter event of DOM.
417   */
418  onDragEnterVolumesList_: function(list, event) {
419    event.preventDefault();  // Required to prevent the cursor flicker.
420    this.lastEnteredTarget_ = event.target;
421    var item = list.getListItemAncestor(event.target);
422    item = item && list.isItem(item) ? item : null;
423    if (item == this.dropTarget_)
424      return;
425
426    var path = item && list.dataModel.item(item.listIndex).path;
427    if (path)
428      this.setDropTarget_(item, true /* directory */, event.dataTransfer, path);
429    else
430      this.clearDropTarget_();
431  },
432
433  /**
434   * @this {FileTransferController}
435   * @param {cr.ui.List} list Drop target list.
436   * @param {Event} event A dragleave event of DOM.
437   */
438  onDragLeave_: function(list, event) {
439    // If mouse moves from one element to another the 'dragenter'
440    // event for the new element comes before the 'dragleave' event for
441    // the old one. In this case event.target != this.lastEnteredTarget_
442    // and handler of the 'dragenter' event has already caried of
443    // drop target. So event.target == this.lastEnteredTarget_
444    // could only be if mouse goes out of listened element.
445    if (event.target == this.lastEnteredTarget_) {
446      this.clearDropTarget_();
447      this.lastEnteredTarget_ = null;
448    }
449  },
450
451  /**
452   * @this {FileTransferController}
453   * @param {boolean} onlyIntoDirectories True if the drag is only into
454   *     directories.
455   * @param {Event} event A dragleave event of DOM.
456   */
457  onDrop_: function(onlyIntoDirectories, event) {
458    if (onlyIntoDirectories && !this.dropTarget_)
459      return;
460    var destinationPath = this.destinationPath_ ||
461                          this.currentDirectoryContentPath;
462    if (!this.canPasteOrDrop_(event.dataTransfer, destinationPath))
463      return;
464    event.preventDefault();
465    this.paste(event.dataTransfer, destinationPath,
466               this.selectDropEffect_(event, destinationPath));
467    this.clearDropTarget_();
468  },
469
470  /**
471   * Sets the drop target.
472   * @this {FileTransferController}
473   * @param {Element} domElement Target of the drop.
474   * @param {boolean} isDirectory If the target is a directory.
475   * @param {DataTransfer} dataTransfer Data transfer object.
476   * @param {string} destinationPath Destination path.
477   */
478  setDropTarget_: function(domElement, isDirectory, dataTransfer,
479                           destinationPath) {
480    if (this.dropTarget_ == domElement)
481      return;
482
483    // Remove the old drop target.
484    this.clearDropTarget_();
485
486    // Set the new drop target.
487    this.dropTarget_ = domElement;
488
489    if (!domElement ||
490        !isDirectory ||
491        !this.canPasteOrDrop_(dataTransfer, destinationPath)) {
492      return;
493    }
494
495    // Add accept class if the domElement can accept the drag.
496    domElement.classList.add('accepts');
497    this.destinationPath_ = destinationPath;
498
499    // Start timer changing the directory.
500    this.navigateTimer_ = setTimeout(function() {
501      if (domElement instanceof DirectoryItem)
502        // Do custom action.
503        (/** @type {DirectoryItem} */ domElement).doDropTargetAction();
504      this.directoryModel_.changeDirectory(destinationPath);
505    }.bind(this), 2000);
506  },
507
508  /**
509   * Handles touch start.
510   */
511  onTouchStart_: function() {
512    this.touching_ = true;
513  },
514
515  /**
516   * Handles touch end.
517   */
518  onTouchEnd_: function(event) {
519    if (event.touches.length === 0)
520      this.touching_ = false;
521  },
522
523  /**
524   * Clears the drop target.
525   * @this {FileTransferController}
526   */
527  clearDropTarget_: function() {
528    if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts'))
529      this.dropTarget_.classList.remove('accepts');
530    this.dropTarget_ = null;
531    this.destinationPath_ = null;
532    if (this.navigateTimer_ !== undefined) {
533      clearTimeout(this.navigateTimer_);
534      this.navigateTimer_ = undefined;
535    }
536  },
537
538  /**
539   * @this {FileTransferController}
540   * @return {boolean} Returns false if {@code <input type="text">} element is
541   *     currently active. Otherwise, returns true.
542   */
543  isDocumentWideEvent_: function() {
544    return this.document_.activeElement.nodeName.toLowerCase() != 'input' ||
545        this.document_.activeElement.type.toLowerCase() != 'text';
546  },
547
548  /**
549   * @this {FileTransferController}
550   */
551  onCopy_: function(event) {
552    if (!this.isDocumentWideEvent_() ||
553        !this.canCopyOrDrag_()) {
554      return;
555    }
556    event.preventDefault();
557    this.cutOrCopy_(event.clipboardData, 'copy');
558    this.notify_('selection-copied');
559  },
560
561  /**
562   * @this {FileTransferController}
563   */
564  onBeforeCopy_: function(event) {
565    if (!this.isDocumentWideEvent_())
566      return;
567
568    // queryCommandEnabled returns true if event.defaultPrevented is true.
569    if (this.canCopyOrDrag_())
570      event.preventDefault();
571  },
572
573  /**
574   * @this {FileTransferController}
575   * @return {boolean} Returns true if some files are selected and all the file
576   *     on drive is available to be copied. Otherwise, returns false.
577   */
578  canCopyOrDrag_: function() {
579    if (this.isOnDrive &&
580        this.directoryModel_.isDriveOffline() &&
581        !this.allDriveFilesAvailable)
582      return false;
583    return this.selectedEntries_.length > 0;
584  },
585
586  /**
587   * @this {FileTransferController}
588   */
589  onCut_: function(event) {
590    if (!this.isDocumentWideEvent_() ||
591        !this.canCutOrDrag_()) {
592      return;
593    }
594    event.preventDefault();
595    this.cutOrCopy_(event.clipboardData, 'move');
596    this.notify_('selection-cut');
597  },
598
599  /**
600   * @this {FileTransferController}
601   */
602  onBeforeCut_: function(event) {
603    if (!this.isDocumentWideEvent_())
604      return;
605    // queryCommandEnabled returns true if event.defaultPrevented is true.
606    if (this.canCutOrDrag_())
607      event.preventDefault();
608  },
609
610  /**
611   * @this {FileTransferController}
612   * @return {boolean} Returns true if some files are selected and all the file
613   *     on drive is available to be cut. Otherwise, returns false.
614   */
615  canCutOrDrag_: function() {
616    return !this.readonly && this.canCopyOrDrag_();
617  },
618
619  /**
620   * @this {FileTransferController}
621   */
622  onPaste_: function(event) {
623    // Need to update here since 'beforepaste' doesn't fire.
624    if (!this.isDocumentWideEvent_() ||
625        !this.canPasteOrDrop_(event.clipboardData,
626                              this.currentDirectoryContentPath)) {
627      return;
628    }
629    event.preventDefault();
630    var effect = this.paste(event.clipboardData);
631
632    // On cut, we clear the clipboard after the file is pasted/moved so we don't
633    // try to move/delete the original file again.
634    if (effect == 'move') {
635      this.simulateCommand_('cut', function(event) {
636        event.preventDefault();
637        event.clipboardData.setData('fs/clear', '');
638      });
639    }
640  },
641
642  /**
643   * @this {FileTransferController}
644   */
645  onBeforePaste_: function(event) {
646    if (!this.isDocumentWideEvent_())
647      return;
648    // queryCommandEnabled returns true if event.defaultPrevented is true.
649    if (this.canPasteOrDrop_(event.clipboardData,
650                             this.currentDirectoryContentPath)) {
651      event.preventDefault();
652    }
653  },
654
655  /**
656   * @this {FileTransferController}
657   * @param {DataTransfer} dataTransfer Data transfer object.
658   * @param {string?} destinationPath Destination path.
659   * @return {boolean} Returns true if items stored in {@code dataTransfer} can
660   *     be pasted to {@code destinationPath}. Otherwise, returns false.
661   */
662  canPasteOrDrop_: function(dataTransfer, destinationPath) {
663    if (!destinationPath) {
664      return false;
665    }
666    if (this.directoryModel_.isPathReadOnly(destinationPath)) {
667      return false;
668    }
669    if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') == -1) {
670      return false;  // Unsupported type of content.
671    }
672    if (dataTransfer.getData('fs/tag') == '') {
673      // Data protected. Other checks are not possible but it makes sense to
674      // let the user try.
675      return true;
676    }
677
678    var directories = dataTransfer.getData('fs/directories').split('\n').
679                      filter(function(d) { return d != ''; });
680
681    for (var i = 0; i < directories.length; i++) {
682      if (destinationPath.substr(0, directories[i].length) == directories[i])
683        return false;  // recursive paste.
684    }
685
686    return true;
687  },
688
689  /**
690   * Execute paste command.
691   *
692   * @this {FileTransferController}
693   * @return {boolean}  Returns true, the paste is success. Otherwise, returns
694   *     false.
695   */
696  queryPasteCommandEnabled: function() {
697    if (!this.isDocumentWideEvent_()) {
698      return false;
699    }
700
701    // HACK(serya): return this.document_.queryCommandEnabled('paste')
702    // should be used.
703    var result;
704    this.simulateCommand_('paste', function(event) {
705      result = this.canPasteOrDrop_(event.clipboardData,
706                                    this.currentDirectoryContentPath);
707    }.bind(this));
708    return result;
709  },
710
711  /**
712   * Allows to simulate commands to get access to clipboard.
713   *
714   * @this {FileTransferController}
715   * @param {string} command 'copy', 'cut' or 'paste'.
716   * @param {function} handler Event handler.
717   */
718  simulateCommand_: function(command, handler) {
719    var iframe = this.document_.querySelector('#command-dispatcher');
720    var doc = iframe.contentDocument;
721    doc.addEventListener(command, handler);
722    doc.execCommand(command);
723    doc.removeEventListener(command, handler);
724  },
725
726  /**
727   * @this {FileTransferController}
728   */
729  onSelectionChanged_: function(event) {
730    var entries = this.selectedEntries_;
731    var files = this.selectedFileObjects_ = [];
732    this.preloadedThumbnailImageNode_ = null;
733
734    var fileEntries = [];
735    for (var i = 0; i < entries.length; i++) {
736      if (entries[i].isFile)
737        fileEntries.push(entries[i]);
738    }
739
740    if (entries.length == 1) {
741      // For single selection, the dragged element is created in advance,
742      // otherwise an image may not be loaded at the time the 'dragstart' event
743      // comes.
744      this.preloadThumbnailImage_(entries[0]);
745    }
746
747    // File object must be prepeared in advance for clipboard operations
748    // (copy, paste and drag). DataTransfer object closes for write after
749    // returning control from that handlers so they may not have
750    // asynchronous operations.
751    var prepareFileObjects = function() {
752      for (var i = 0; i < fileEntries.length; i++) {
753        fileEntries[i].file(function(file) { files.push(file); });
754      }
755    };
756
757    if (this.isOnDrive) {
758      this.allDriveFilesAvailable = false;
759      this.metadataCache_.get(
760          entries, 'drive', function(props) {
761        // We consider directories not available offline for the purposes of
762        // file transfer since we cannot afford to recursive traversal.
763        this.allDriveFilesAvailable =
764            entries.filter(function(e) {return e.isDirectory}).length == 0 &&
765            props.filter(function(p) {return !p.availableOffline}).length == 0;
766        // |Copy| is the only menu item affected by allDriveFilesAvailable.
767        // It could be open right now, update its UI.
768        this.copyCommand_.disabled = !this.canCopyOrDrag_();
769
770        if (this.allDriveFilesAvailable)
771          prepareFileObjects();
772      }.bind(this));
773    } else {
774      prepareFileObjects();
775    }
776  },
777
778  /**
779   * Path of directory that is displaying now.
780   * If search result is displaying now, this is null.
781   * @this {FileTransferController}
782   * @return {string} Path of directry that is displaying now.
783   */
784  get currentDirectoryContentPath() {
785    return this.directoryModel_.isSearching() ?
786        null : this.directoryModel_.getCurrentDirPath();
787  },
788
789  /**
790   * @this {FileTransferController}
791   * @return {boolean} True if the current directory is read only.
792   */
793  get readonly() {
794    return this.directoryModel_.isReadOnly();
795  },
796
797  /**
798   * @this {FileTransferController}
799   * @return {boolean} True if the current directory is on Drive.
800   */
801  get isOnDrive() {
802    return PathUtil.isDriveBasedPath(this.directoryModel_.getCurrentRootPath());
803  },
804
805  /**
806   * @this {FileTransferController}
807   */
808  notify_: function(eventName) {
809    var self = this;
810    // Set timeout to avoid recursive events.
811    setTimeout(function() {
812      cr.dispatchSimpleEvent(self, eventName);
813    }, 0);
814  },
815
816  /**
817   * @this {FileTransferController}
818   * @return {Array.<Entry>} Array of the selected entries.
819   */
820  get selectedEntries_() {
821    var list = this.directoryModel_.getFileList();
822    var selectedIndexes = this.directoryModel_.getFileListSelection().
823                               selectedIndexes;
824    var entries = selectedIndexes.map(function(index) {
825      return list.item(index);
826    });
827
828    // TODO(serya): Diagnostics for http://crbug/129642
829    if (entries.indexOf(undefined) != -1) {
830      var index = entries.indexOf(undefined);
831      entries = entries.filter(function(e) { return !!e; });
832      console.error('Invalid selection found: list items: ', list.length,
833                    'wrong indexe value: ', selectedIndexes[index],
834                    'Stack trace: ', new Error().stack);
835    }
836    return entries;
837  },
838
839  /**
840   * @this {FileTransferController}
841   * @return {string}  Returns the appropriate drop query type ('none', 'move'
842   *     or copy') to the current modifiers status and the destination.
843   */
844  selectDropEffect_: function(event, destinationPath) {
845    if (!destinationPath ||
846        this.directoryModel_.isPathReadOnly(destinationPath))
847      return 'none';
848    if (event.dataTransfer.effectAllowed == 'copyMove' &&
849        this.getSourceRoot_(event.dataTransfer) ==
850            PathUtil.getRootPath(destinationPath) &&
851        !event.ctrlKey) {
852      return 'move';
853    }
854    if (event.dataTransfer.effectAllowed == 'copyMove' &&
855        event.shiftKey) {
856      return 'move';
857    }
858    return 'copy';
859  },
860};
861