cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / foreground / js / file_transfer_controller.js
blobffe7c334ce6826dc3077e77e9a6c2651d548c728
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.
5 'use strict';
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';
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  */
23 function FileTransferController(doc,
24                                 fileOperationManager,
25                                 metadataCache,
26                                 directoryModel) {
27   this.document_ = doc;
28   this.fileOperationManager_ = fileOperationManager;
29   this.metadataCache_ = metadataCache;
30   this.directoryModel_ = directoryModel;
32   this.directoryModel_.getFileListSelection().addEventListener('change',
33       this.onSelectionChanged_.bind(this));
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;
43   /**
44    * File objects for selected files.
45    *
46    * @type {Array.<File>}
47    * @private
48    */
49   this.selectedFileObjects_ = [];
51   /**
52    * Drag selector.
53    * @type {DragSelector}
54    * @private
55    */
56   this.dragSelector_ = new DragSelector();
58   /**
59    * Whether a user is touching the device or not.
60    * @type {boolean}
61    * @private
62    */
63   this.touching_ = false;
66 FileTransferController.prototype = {
67   __proto__: cr.EventTarget.prototype,
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   },
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   },
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   },
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   },
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   },
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);
164     for (var i = 0; i < this.selectedFileObjects_.length; i++) {
165       dataTransfer.items.add(this.selectedFileObjects_[i]);
166     }
167   },
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;
181     // |dataTransfer| in protected mode.
182     if (window[DRAG_AND_DROP_GLOBAL_DATA])
183       return window[DRAG_AND_DROP_GLOBAL_DATA].sourceRoot;
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     }
192     // Unknown source.
193     return '';
194   },
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');
218     // Start the pasting operation.
219     this.fileOperationManager_.paste(sourcePaths, destinationPath, toMove);
220     return toMove ? 'move' : 'copy';
221   },
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 imageUrl = entry.toURL();
231     var metadataTypes = 'thumbnail|filesystem';
232     var thumbnailContainer = this.document_.createElement('div');
233     this.preloadedThumbnailImageNode_ = thumbnailContainer;
234     this.preloadedThumbnailImageNode_.className = 'img-container';
235     this.metadataCache_.get(
236         imageUrl,
237         metadataTypes,
238         function(metadata) {
239           new ThumbnailLoader(imageUrl,
240                               ThumbnailLoader.LoaderType.IMAGE,
241                               metadata).
242               load(thumbnailContainer,
243                    ThumbnailLoader.FillMode.FILL);
244         }.bind(this));
245   },
247   /**
248    * Renders a drag-and-drop thumbnail.
249    *
250    * @this {FileTransferController}
251    * @return {HTMLElement} Element containing the thumbnail.
252    */
253   renderThumbnail_: function() {
254     var length = this.selectedEntries_.length;
256     var container = this.document_.querySelector('#drag-container');
257     var contents = this.document_.createElement('div');
258     contents.className = 'drag-contents';
259     container.appendChild(contents);
261     var thumbnailImage;
262     if (this.preloadedThumbnailImageNode_)
263       thumbnailImage = this.preloadedThumbnailImageNode_.querySelector('img');
265     // Option 1. Multiple selection, render only a label.
266     if (length > 1) {
267       var label = this.document_.createElement('div');
268       label.className = 'label';
269       label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
270       contents.appendChild(label);
271       return container;
272     }
274     // Option 2. Thumbnail image available, then render it without
275     // a label.
276     if (thumbnailImage) {
277       thumbnailImage.classList.add('drag-thumbnail');
278       contents.classList.add('for-image');
279       contents.appendChild(this.preloadedThumbnailImageNode_);
280       return container;
281     }
283     // Option 3. Thumbnail not available. Render an icon and a label.
284     var entry = this.selectedEntries_[0];
285     var icon = this.document_.createElement('div');
286     icon.className = 'detail-icon';
287     icon.setAttribute('file-type-icon', FileType.getIcon(entry));
288     contents.appendChild(icon);
289     var label = this.document_.createElement('div');
290     label.className = 'label';
291     label.textContent = entry.name;
292     contents.appendChild(label);
293     return container;
294   },
296   /**
297    * @this {FileTransferController}
298    * @param {cr.ui.List} list Drop target list
299    * @param {Event} event A dragstart event of DOM.
300    */
301   onDragStart_: function(list, event) {
302     // If a user is touching, Files.app does not receive drag operations.
303     if (this.touching_) {
304       event.preventDefault();
305       return;
306     }
308     // Check if a drag selection should be initiated or not.
309     if (list.shouldStartDragSelection(event)) {
310       this.dragSelector_.startDragSelection(list, event);
311       return;
312     }
314     // Nothing selected.
315     if (!this.selectedEntries_.length) {
316       event.preventDefault();
317       return;
318     }
320     var dt = event.dataTransfer;
322     if (this.canCopyOrDrag_(dt)) {
323       if (this.canCutOrDrag_(dt))
324         this.cutOrCopy_(dt, 'copyMove');
325       else
326         this.cutOrCopy_(dt, 'copy');
327     } else {
328       event.preventDefault();
329       return;
330     }
332     var dragThumbnail = this.renderThumbnail_();
333     dt.setDragImage(dragThumbnail, 1000, 1000);
335     window[DRAG_AND_DROP_GLOBAL_DATA] = {
336       sourceRoot: this.directoryModel_.getCurrentRootPath()
337     };
338   },
340   /**
341    * @this {FileTransferController}
342    * @param {cr.ui.List} list Drop target list.
343    * @param {Event} event A dragend event of DOM.
344    */
345   onDragEnd_: function(list, event) {
346     var container = this.document_.querySelector('#drag-container');
347     container.textContent = '';
348     this.clearDropTarget_();
349     delete window[DRAG_AND_DROP_GLOBAL_DATA];
350   },
352   /**
353    * @this {FileTransferController}
354    * @param {boolean} onlyIntoDirectories True if the drag is only into
355    *     directories.
356    * @param {cr.ui.List} list Drop target list.
357    * @param {Event} event A dragover event of DOM.
358    */
359   onDragOver_: function(onlyIntoDirectories, list, event) {
360     event.preventDefault();
361     var path = this.destinationPath_ ||
362         (!onlyIntoDirectories && this.currentDirectoryContentPath);
363     event.dataTransfer.dropEffect = this.selectDropEffect_(event, path);
364     event.preventDefault();
365   },
367   /**
368    * @this {FileTransferController}
369    * @param {cr.ui.List} list Drop target list.
370    * @param {Event} event A dragenter event of DOM.
371    */
372   onDragEnterFileList_: function(list, event) {
373     event.preventDefault();  // Required to prevent the cursor flicker.
374     this.lastEnteredTarget_ = event.target;
375     var item = list.getListItemAncestor(event.target);
376     item = item && list.isItem(item) ? item : null;
377     if (item == this.dropTarget_)
378       return;
380     var entry = item && list.dataModel.item(item.listIndex);
381     if (entry) {
382       this.setDropTarget_(item, entry.isDirectory, event.dataTransfer,
383           entry.fullPath);
384     } else {
385       this.clearDropTarget_();
386     }
387   },
389   /**
390    * @this {FileTransferController}
391    * @param {DirectoryTree} tree Drop target tree.
392    * @param {Event} event A dragenter event of DOM.
393    */
394   onDragEnterTree_: function(tree, event) {
395     event.preventDefault();  // Required to prevent the cursor flicker.
396     this.lastEnteredTarget_ = event.target;
397     var item = event.target;
398     while (item && !(item instanceof DirectoryItem)) {
399       item = item.parentNode;
400     }
402     if (item == this.dropTarget_)
403       return;
405     var entry = item && item.entry;
406     if (entry) {
407       this.setDropTarget_(item, entry.isDirectory, event.dataTransfer,
408           entry.fullPath);
409     } else {
410       this.clearDropTarget_();
411     }
412   },
414   /**
415    * @this {FileTransferController}
416    * @param {NavigationList} list Drop target list.
417    * @param {Event} event A dragenter event of DOM.
418    */
419   onDragEnterVolumesList_: function(list, event) {
420     event.preventDefault();  // Required to prevent the cursor flicker.
421     this.lastEnteredTarget_ = event.target;
422     var item = list.getListItemAncestor(event.target);
423     item = item && list.isItem(item) ? item : null;
424     if (item == this.dropTarget_)
425       return;
427     var path = item && list.dataModel.item(item.listIndex).path;
428     if (path)
429       this.setDropTarget_(item, true /* directory */, event.dataTransfer, path);
430     else
431       this.clearDropTarget_();
432   },
434   /**
435    * @this {FileTransferController}
436    * @param {cr.ui.List} list Drop target list.
437    * @param {Event} event A dragleave event of DOM.
438    */
439   onDragLeave_: function(list, event) {
440     // If mouse moves from one element to another the 'dragenter'
441     // event for the new element comes before the 'dragleave' event for
442     // the old one. In this case event.target != this.lastEnteredTarget_
443     // and handler of the 'dragenter' event has already caried of
444     // drop target. So event.target == this.lastEnteredTarget_
445     // could only be if mouse goes out of listened element.
446     if (event.target == this.lastEnteredTarget_) {
447       this.clearDropTarget_();
448       this.lastEnteredTarget_ = null;
449     }
450   },
452   /**
453    * @this {FileTransferController}
454    * @param {boolean} onlyIntoDirectories True if the drag is only into
455    *     directories.
456    * @param {Event} event A dragleave event of DOM.
457    */
458   onDrop_: function(onlyIntoDirectories, event) {
459     if (onlyIntoDirectories && !this.dropTarget_)
460       return;
461     var destinationPath = this.destinationPath_ ||
462                           this.currentDirectoryContentPath;
463     if (!this.canPasteOrDrop_(event.dataTransfer, destinationPath))
464       return;
465     event.preventDefault();
466     this.paste(event.dataTransfer, destinationPath,
467                this.selectDropEffect_(event, destinationPath));
468     this.clearDropTarget_();
469   },
471   /**
472    * Sets the drop target.
473    * @this {FileTransferController}
474    * @param {Element} domElement Target of the drop.
475    * @param {boolean} isDirectory If the target is a directory.
476    * @param {DataTransfer} dataTransfer Data transfer object.
477    * @param {string} destinationPath Destination path.
478    */
479   setDropTarget_: function(domElement, isDirectory, dataTransfer,
480                            destinationPath) {
481     if (this.dropTarget_ == domElement)
482       return;
484     // Remove the old drop target.
485     this.clearDropTarget_();
487     // Set the new drop target.
488     this.dropTarget_ = domElement;
490     if (!domElement ||
491         !isDirectory ||
492         !this.canPasteOrDrop_(dataTransfer, destinationPath)) {
493       return;
494     }
496     // Add accept class if the domElement can accept the drag.
497     domElement.classList.add('accepts');
498     this.destinationPath_ = destinationPath;
500     // Start timer changing the directory.
501     this.navigateTimer_ = setTimeout(function() {
502       if (domElement instanceof DirectoryItem)
503         // Do custom action.
504         (/** @type {DirectoryItem} */ domElement).doDropTargetAction();
505       this.directoryModel_.changeDirectory(destinationPath);
506     }.bind(this), 2000);
507   },
509   /**
510    * Handles touch start.
511    */
512   onTouchStart_: function() {
513     this.touching_ = true;
514   },
516   /**
517    * Handles touch end.
518    */
519   onTouchEnd_: function(event) {
520     if (event.touches.length === 0)
521       this.touching_ = false;
522   },
524   /**
525    * Clears the drop target.
526    * @this {FileTransferController}
527    */
528   clearDropTarget_: function() {
529     if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts'))
530       this.dropTarget_.classList.remove('accepts');
531     this.dropTarget_ = null;
532     this.destinationPath_ = null;
533     if (this.navigateTimer_ !== undefined) {
534       clearTimeout(this.navigateTimer_);
535       this.navigateTimer_ = undefined;
536     }
537   },
539   /**
540    * @this {FileTransferController}
541    * @return {boolean} Returns false if {@code <input type="text">} element is
542    *     currently active. Otherwise, returns true.
543    */
544   isDocumentWideEvent_: function() {
545     return this.document_.activeElement.nodeName.toLowerCase() != 'input' ||
546         this.document_.activeElement.type.toLowerCase() != 'text';
547   },
549   /**
550    * @this {FileTransferController}
551    */
552   onCopy_: function(event) {
553     if (!this.isDocumentWideEvent_() ||
554         !this.canCopyOrDrag_()) {
555       return;
556     }
557     event.preventDefault();
558     this.cutOrCopy_(event.clipboardData, 'copy');
559     this.notify_('selection-copied');
560   },
562   /**
563    * @this {FileTransferController}
564    */
565   onBeforeCopy_: function(event) {
566     if (!this.isDocumentWideEvent_())
567       return;
569     // queryCommandEnabled returns true if event.defaultPrevented is true.
570     if (this.canCopyOrDrag_())
571       event.preventDefault();
572   },
574   /**
575    * @this {FileTransferController}
576    * @return {boolean} Returns true if some files are selected and all the file
577    *     on drive is available to be copied. Otherwise, returns false.
578    */
579   canCopyOrDrag_: function() {
580     if (this.isOnDrive &&
581         this.directoryModel_.isDriveOffline() &&
582         !this.allDriveFilesAvailable)
583       return false;
584     return this.selectedEntries_.length > 0;
585   },
587   /**
588    * @this {FileTransferController}
589    */
590   onCut_: function(event) {
591     if (!this.isDocumentWideEvent_() ||
592         !this.canCutOrDrag_()) {
593       return;
594     }
595     event.preventDefault();
596     this.cutOrCopy_(event.clipboardData, 'move');
597     this.notify_('selection-cut');
598   },
600   /**
601    * @this {FileTransferController}
602    */
603   onBeforeCut_: function(event) {
604     if (!this.isDocumentWideEvent_())
605       return;
606     // queryCommandEnabled returns true if event.defaultPrevented is true.
607     if (this.canCutOrDrag_())
608       event.preventDefault();
609   },
611   /**
612    * @this {FileTransferController}
613    * @return {boolean} Returns true if some files are selected and all the file
614    *     on drive is available to be cut. Otherwise, returns false.
615    */
616   canCutOrDrag_: function() {
617     return !this.readonly && this.canCopyOrDrag_();
618   },
620   /**
621    * @this {FileTransferController}
622    */
623   onPaste_: function(event) {
624     // Need to update here since 'beforepaste' doesn't fire.
625     if (!this.isDocumentWideEvent_() ||
626         !this.canPasteOrDrop_(event.clipboardData,
627                               this.currentDirectoryContentPath)) {
628       return;
629     }
630     event.preventDefault();
631     var effect = this.paste(event.clipboardData);
633     // On cut, we clear the clipboard after the file is pasted/moved so we don't
634     // try to move/delete the original file again.
635     if (effect == 'move') {
636       this.simulateCommand_('cut', function(event) {
637         event.preventDefault();
638         event.clipboardData.setData('fs/clear', '');
639       });
640     }
641   },
643   /**
644    * @this {FileTransferController}
645    */
646   onBeforePaste_: function(event) {
647     if (!this.isDocumentWideEvent_())
648       return;
649     // queryCommandEnabled returns true if event.defaultPrevented is true.
650     if (this.canPasteOrDrop_(event.clipboardData,
651                              this.currentDirectoryContentPath)) {
652       event.preventDefault();
653     }
654   },
656   /**
657    * @this {FileTransferController}
658    * @param {DataTransfer} dataTransfer Data transfer object.
659    * @param {string?} destinationPath Destination path.
660    * @return {boolean} Returns true if items stored in {@code dataTransfer} can
661    *     be pasted to {@code destinationPath}. Otherwise, returns false.
662    */
663   canPasteOrDrop_: function(dataTransfer, destinationPath) {
664     if (!destinationPath) {
665       return false;
666     }
667     if (this.directoryModel_.isPathReadOnly(destinationPath)) {
668       return false;
669     }
670     if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') == -1) {
671       return false;  // Unsupported type of content.
672     }
673     if (dataTransfer.getData('fs/tag') == '') {
674       // Data protected. Other checks are not possible but it makes sense to
675       // let the user try.
676       return true;
677     }
679     var directories = dataTransfer.getData('fs/directories').split('\n').
680                       filter(function(d) { return d != ''; });
682     for (var i = 0; i < directories.length; i++) {
683       if (destinationPath.substr(0, directories[i].length) == directories[i])
684         return false;  // recursive paste.
685     }
687     return true;
688   },
690   /**
691    * Execute paste command.
692    *
693    * @this {FileTransferController}
694    * @return {boolean}  Returns true, the paste is success. Otherwise, returns
695    *     false.
696    */
697   queryPasteCommandEnabled: function() {
698     if (!this.isDocumentWideEvent_()) {
699       return false;
700     }
702     // HACK(serya): return this.document_.queryCommandEnabled('paste')
703     // should be used.
704     var result;
705     this.simulateCommand_('paste', function(event) {
706       result = this.canPasteOrDrop_(event.clipboardData,
707                                     this.currentDirectoryContentPath);
708     }.bind(this));
709     return result;
710   },
712   /**
713    * Allows to simulate commands to get access to clipboard.
714    *
715    * @this {FileTransferController}
716    * @param {string} command 'copy', 'cut' or 'paste'.
717    * @param {function} handler Event handler.
718    */
719   simulateCommand_: function(command, handler) {
720     var iframe = this.document_.querySelector('#command-dispatcher');
721     var doc = iframe.contentDocument;
722     doc.addEventListener(command, handler);
723     doc.execCommand(command);
724     doc.removeEventListener(command, handler);
725   },
727   /**
728    * @this {FileTransferController}
729    */
730   onSelectionChanged_: function(event) {
731     var entries = this.selectedEntries_;
732     var files = this.selectedFileObjects_ = [];
733     this.preloadedThumbnailImageNode_ = null;
735     var fileEntries = [];
736     for (var i = 0; i < entries.length; i++) {
737       if (entries[i].isFile)
738         fileEntries.push(entries[i]);
739     }
741     if (entries.length == 1) {
742       // For single selection, the dragged element is created in advance,
743       // otherwise an image may not be loaded at the time the 'dragstart' event
744       // comes.
745       this.preloadThumbnailImage_(entries[0]);
746     }
748     // File object must be prepeared in advance for clipboard operations
749     // (copy, paste and drag). DataTransfer object closes for write after
750     // returning control from that handlers so they may not have
751     // asynchronous operations.
752     var prepareFileObjects = function() {
753       for (var i = 0; i < fileEntries.length; i++) {
754         fileEntries[i].file(function(file) { files.push(file); });
755       }
756     };
758     if (this.isOnDrive) {
759       this.allDriveFilesAvailable = false;
760       var urls = entries.map(function(e) { return e.toURL() });
761       this.metadataCache_.get(
762           urls, 'drive', function(props) {
763         // We consider directories not available offline for the purposes of
764         // file transfer since we cannot afford to recursive traversal.
765         this.allDriveFilesAvailable =
766             entries.filter(function(e) {return e.isDirectory}).length == 0 &&
767             props.filter(function(p) {return !p.availableOffline}).length == 0;
768         // |Copy| is the only menu item affected by allDriveFilesAvailable.
769         // It could be open right now, update its UI.
770         this.copyCommand_.disabled = !this.canCopyOrDrag_();
772         if (this.allDriveFilesAvailable)
773           prepareFileObjects();
774       }.bind(this));
775     } else {
776       prepareFileObjects();
777     }
778   },
780   /**
781    * Path of directory that is displaying now.
782    * If search result is displaying now, this is null.
783    * @this {FileTransferController}
784    * @return {string} Path of directry that is displaying now.
785    */
786   get currentDirectoryContentPath() {
787     return this.directoryModel_.isSearching() ?
788         null : this.directoryModel_.getCurrentDirPath();
789   },
791   /**
792    * @this {FileTransferController}
793    * @return {boolean} True if the current directory is read only.
794    */
795   get readonly() {
796     return this.directoryModel_.isReadOnly();
797   },
799   /**
800    * @this {FileTransferController}
801    * @return {boolean} True if the current directory is on Drive.
802    */
803   get isOnDrive() {
804     return PathUtil.isDriveBasedPath(this.directoryModel_.getCurrentRootPath());
805   },
807   /**
808    * @this {FileTransferController}
809    */
810   notify_: function(eventName) {
811     var self = this;
812     // Set timeout to avoid recursive events.
813     setTimeout(function() {
814       cr.dispatchSimpleEvent(self, eventName);
815     }, 0);
816   },
818   /**
819    * @this {FileTransferController}
820    * @return {Array.<Entry>} Array of the selected entries.
821    */
822   get selectedEntries_() {
823     var list = this.directoryModel_.getFileList();
824     var selectedIndexes = this.directoryModel_.getFileListSelection().
825                                selectedIndexes;
826     var entries = selectedIndexes.map(function(index) {
827       return list.item(index);
828     });
830     // TODO(serya): Diagnostics for http://crbug/129642
831     if (entries.indexOf(undefined) != -1) {
832       var index = entries.indexOf(undefined);
833       entries = entries.filter(function(e) { return !!e; });
834       console.error('Invalid selection found: list items: ', list.length,
835                     'wrong indexe value: ', selectedIndexes[index],
836                     'Stack trace: ', new Error().stack);
837     }
838     return entries;
839   },
841   /**
842    * @this {FileTransferController}
843    * @return {string}  Returns the appropriate drop query type ('none', 'move'
844    *     or copy') to the current modifiers status and the destination.
845    */
846   selectDropEffect_: function(event, destinationPath) {
847     if (!destinationPath ||
848         this.directoryModel_.isPathReadOnly(destinationPath))
849       return 'none';
850     if (event.dataTransfer.effectAllowed == 'copyMove' &&
851         this.getSourceRoot_(event.dataTransfer) ==
852             PathUtil.getRootPath(destinationPath) &&
853         !event.ctrlKey) {
854       return 'move';
855     }
856     if (event.dataTransfer.effectAllowed == 'copyMove' &&
857         event.shiftKey) {
858       return 'move';
859     }
860     return 'copy';
861   },