blob: fc43cc1c221dfc7f15d0517a10d0324bd15f08b0 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Global (placed in the window object) variable name to hold internal
* file dragging information. Needed to show visual feedback while dragging
* since DataTransfer object is in protected state. Reachable from other
* file manager instances.
*/
var DRAG_AND_DROP_GLOBAL_DATA = '__drag_and_drop_global_data';
/**
* @param {HTMLDocument} doc Owning document.
* @param {FileOperationManager} fileOperationManager File operation manager
* instance.
* @param {MetadataCache} metadataCache Metadata cache service.
* @param {DirectoryModel} directoryModel Directory model instance.
* @param {VolumeManagerWrapper} volumeManager Volume manager instance.
* @param {MultiProfileShareDialog} multiProfileShareDialog Share dialog to be
* used to share files from another profile.
* @param {ProgressCenter} progressCenter To notify starting copy operation.
* @constructor
* @extends {cr.EventTarget}
*/
function FileTransferController(doc,
fileOperationManager,
metadataCache,
directoryModel,
volumeManager,
multiProfileShareDialog,
progressCenter) {
this.document_ = doc;
this.fileOperationManager_ = fileOperationManager;
this.metadataCache_ = metadataCache;
this.directoryModel_ = directoryModel;
this.volumeManager_ = volumeManager;
this.multiProfileShareDialog_ = multiProfileShareDialog;
this.progressCenter_ = progressCenter;
this.directoryModel_.getFileList().addEventListener(
'change',
function(event) {
if (this.directoryModel_.getFileListSelection().
getIndexSelected(event.index)) {
this.onSelectionChanged_();
}
}.bind(this));
this.directoryModel_.getFileListSelection().addEventListener('change',
this.onSelectionChanged_.bind(this));
/**
* The array of pending task ID.
* @type {Array.<string>}
*/
this.pendingTaskIds = [];
/**
* Promise to be fulfilled with the thumbnail image of selected file in drag
* operation. Used if only one element is selected.
* @type {Promise}
* @private
*/
this.preloadedThumbnailImagePromise_ = null;
/**
* File objects for selected files.
*
* @type {Array.<File>}
* @private
*/
this.selectedFileObjects_ = [];
/**
* Drag selector.
* @type {DragSelector}
* @private
*/
this.dragSelector_ = new DragSelector();
/**
* Whether a user is touching the device or not.
* @type {boolean}
* @private
*/
this.touching_ = false;
/**
* Task ID counter.
* @type {number}
* @private
*/
this.taskIdCounter_ = 0;
}
/**
* Size of drag thumbnail for image files.
*
* @type {number}
* @const
* @private
*/
FileTransferController.DRAG_THUMBNAIL_SIZE_ = 64;
FileTransferController.prototype = {
__proto__: cr.EventTarget.prototype,
/**
* @this {FileTransferController}
* @param {cr.ui.List} list Items in the list will be draggable.
*/
attachDragSource: function(list) {
list.style.webkitUserDrag = 'element';
list.addEventListener('dragstart', this.onDragStart_.bind(this, list));
list.addEventListener('dragend', this.onDragEnd_.bind(this, list));
list.addEventListener('touchstart', this.onTouchStart_.bind(this));
list.ownerDocument.addEventListener(
'touchend', this.onTouchEnd_.bind(this), true);
list.ownerDocument.addEventListener(
'touchcancel', this.onTouchEnd_.bind(this), true);
},
/**
* @this {FileTransferController}
* @param {cr.ui.List} list List itself and its directory items will could
* be drop target.
* @param {boolean=} opt_onlyIntoDirectories If true only directory list
* items could be drop targets. Otherwise any other place of the list
* accetps files (putting it into the current directory).
*/
attachFileListDropTarget: function(list, opt_onlyIntoDirectories) {
list.addEventListener('dragover', this.onDragOver_.bind(this,
!!opt_onlyIntoDirectories, list));
list.addEventListener('dragenter',
this.onDragEnterFileList_.bind(this, list));
list.addEventListener('dragleave', this.onDragLeave_.bind(this, list));
list.addEventListener('drop',
this.onDrop_.bind(this, !!opt_onlyIntoDirectories));
},
/**
* @this {FileTransferController}
* @param {DirectoryTree} tree Its sub items will could be drop target.
*/
attachTreeDropTarget: function(tree) {
tree.addEventListener('dragover', this.onDragOver_.bind(this, true, tree));
tree.addEventListener('dragenter', this.onDragEnterTree_.bind(this, tree));
tree.addEventListener('dragleave', this.onDragLeave_.bind(this, tree));
tree.addEventListener('drop', this.onDrop_.bind(this, true));
},
/**
* Attach handlers of copy, cut and paste operations to the document.
*
* @this {FileTransferController}
*/
attachCopyPasteHandlers: function() {
this.document_.addEventListener('beforecopy',
this.onBeforeCopy_.bind(this));
this.document_.addEventListener('copy',
this.onCopy_.bind(this));
this.document_.addEventListener('beforecut',
this.onBeforeCut_.bind(this));
this.document_.addEventListener('cut',
this.onCut_.bind(this));
this.document_.addEventListener('beforepaste',
this.onBeforePaste_.bind(this));
this.document_.addEventListener('paste',
this.onPaste_.bind(this));
this.copyCommand_ = this.document_.querySelector('command#copy');
},
/**
* Write the current selection to system clipboard.
*
* @this {FileTransferController}
* @param {DataTransfer} dataTransfer DataTransfer from the event.
* @param {string} effectAllowed Value must be valid for the
* |dataTransfer.effectAllowed| property.
*/
cutOrCopy_: function(dataTransfer, effectAllowed) {
// Existence of the volumeInfo is checked in canXXX methods.
var volumeInfo = this.volumeManager_.getVolumeInfo(
this.currentDirectoryContentEntry);
// Tag to check it's filemanager data.
dataTransfer.setData('fs/tag', 'filemanager-data');
dataTransfer.setData('fs/sourceRootURL',
volumeInfo.fileSystem.root.toURL());
var sourceURLs = util.entriesToURLs(this.selectedEntries_);
dataTransfer.setData('fs/sources', sourceURLs.join('\n'));
dataTransfer.effectAllowed = effectAllowed;
dataTransfer.setData('fs/effectallowed', effectAllowed);
dataTransfer.setData('fs/missingFileContents',
(!this.isAllSelectedFilesAvailable_()).toString());
for (var i = 0; i < this.selectedFileObjects_.length; i++) {
dataTransfer.items.add(this.selectedFileObjects_[i]);
}
},
/**
* @this {FileTransferController}
* @return {Object.<string, string>} Drag and drop global data object.
*/
getDragAndDropGlobalData_: function() {
if (window[DRAG_AND_DROP_GLOBAL_DATA])
return window[DRAG_AND_DROP_GLOBAL_DATA];
// Dragging from other tabs/windows.
var views = chrome && chrome.extension ? chrome.extension.getViews() : [];
for (var i = 0; i < views.length; i++) {
if (views[i][DRAG_AND_DROP_GLOBAL_DATA])
return views[i][DRAG_AND_DROP_GLOBAL_DATA];
}
return null;
},
/**
* Extracts source root URL from the |dataTransfer| object.
*
* @this {FileTransferController}
* @param {DataTransfer} dataTransfer DataTransfer object from the event.
* @return {string} URL or an empty string (if unknown).
*/
getSourceRootURL_: function(dataTransfer) {
var sourceRootURL = dataTransfer.getData('fs/sourceRootURL');
if (sourceRootURL)
return sourceRootURL;
// |dataTransfer| in protected mode.
var globalData = this.getDragAndDropGlobalData_();
if (globalData)
return globalData.sourceRootURL;
// Unknown source.
return '';
},
/**
* @this {FileTransferController}
* @param {DataTransfer} dataTransfer DataTransfer object from the event.
* @return {boolean} Returns true when missing some file contents.
*/
isMissingFileContents_: function(dataTransfer) {
var data = dataTransfer.getData('fs/missingFileContents');
if (!data) {
// |dataTransfer| in protected mode.
var globalData = this.getDragAndDropGlobalData_();
if (globalData)
data = globalData.missingFileContents;
}
return data === 'true';
},
/**
* Obtains entries that need to share with me.
* The method also observers child entries of the given entries.
* @param {Array.<Entry>} entries Entries.
* @return {Promise} Promise to be fulfilled with the entries that need to
* share.
*/
getMultiProfileShareEntries_: function(entries) {
// Utility function to concat arrays.
var concatArrays = function(arrays) {
return Array.prototype.concat.apply([], arrays);
};
// Call processEntry for each item of entries.
var processEntries = function(entries) {
var files = entries.filter(function(entry) {return entry.isFile;});
var dirs = entries.filter(function(entry) {return !entry.isFile;});
var promises = dirs.map(processDirectoryEntry);
if (files.length > 0)
promises.push(processFileEntries(files));
return Promise.all(promises).then(concatArrays);
};
// Check all file entries and keeps only those need sharing operation.
var processFileEntries = function(entries) {
return new Promise(function(callback) {
// TODO(mtomasz): Move conversion from entry to url to custom bindings.
// crbug.com/345527.
var urls = util.entriesToURLs(entries);
chrome.fileManagerPrivate.getEntryProperties(urls, callback);
}).then(function(metadatas) {
return entries.filter(function(entry, i) {
var metadata = metadatas[i];
return metadata && metadata.isHosted && !metadata.sharedWithMe;
});
});
};
// Check child entries.
var processDirectoryEntry = function(entry) {
return readEntries(entry.createReader());
};
// Read entries from DirectoryReader and call processEntries for the chunk
// of entries.
var readEntries = function(reader) {
return new Promise(reader.readEntries.bind(reader)).then(
function(entries) {
if (entries.length > 0) {
return Promise.all(
[processEntries(entries), readEntries(reader)]).
then(concatArrays);
} else {
return [];
}
},
function(error) {
console.warn(
'Error happens while reading directory.', error);
return [];
});
}.bind(this);
// Filter entries that is owned by the current user, and call
// processEntries.
return processEntries(entries.filter(function(entry) {
// If the volumeInfo is found, the entry belongs to the current user.
return !this.volumeManager_.getVolumeInfo(entry);
}.bind(this)));
},
/**
* Queue up a file copy operation based on the current system clipboard.
*
* @this {FileTransferController}
* @param {DataTransfer} dataTransfer System data transfer object.
* @param {DirectoryEntry=} opt_destinationEntry Paste destination.
* @param {string=} opt_effect Desired drop/paste effect. Could be
* 'move'|'copy' (default is copy). Ignored if conflicts with
* |dataTransfer.effectAllowed|.
* @return {string} Either "copy" or "move".
*/
paste: function(dataTransfer, opt_destinationEntry, opt_effect) {
var sourceURLs = dataTransfer.getData('fs/sources') ?
dataTransfer.getData('fs/sources').split('\n') : [];
// effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers
// work fine.
var effectAllowed = dataTransfer.effectAllowed !== 'uninitialized' ?
dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed');
var toMove = util.isDropEffectAllowed(effectAllowed, 'move') &&
(!util.isDropEffectAllowed(effectAllowed, 'copy') ||
opt_effect === 'move');
var destinationEntry =
opt_destinationEntry || this.currentDirectoryContentEntry;
var entries = [];
var failureUrls;
var taskId = this.fileOperationManager_.generateTaskId();
util.URLsToEntries(sourceURLs).then(function(result) {
failureUrls = result.failureUrls;
return this.fileOperationManager_.filterSameDirectoryEntry(
result.entries, destinationEntry, toMove);
}.bind(this)).then(function(filteredEntries) {
entries = filteredEntries;
if (entries.length === 0)
return Promise.reject('ABORT');
this.pendingTaskIds.push(taskId);
var item = new ProgressCenterItem();
item.id = taskId;
if (toMove) {
item.type = ProgressItemType.MOVE;
if (entries.length === 1)
item.message = strf('MOVE_FILE_NAME', entries[0].name);
else
item.message = strf('MOVE_ITEMS_REMAINING', entries.length);
} else {
item.type = ProgressItemType.COPY;
if (entries.length === 1)
item.message = strf('COPY_FILE_NAME', entries[0].name);
else
item.message = strf('COPY_ITEMS_REMAINING', entries.length);
}
this.progressCenter_.updateItem(item);
// Check if cross share is needed or not.
return this.getMultiProfileShareEntries_(entries);
}.bind(this)).then(function(shareEntries) {
if (shareEntries.length === 0)
return;
return this.multiProfileShareDialog_.show(shareEntries.length > 1).
then(function(dialogResult) {
if (dialogResult === 'cancel')
return Promise.reject('ABORT');
// Do cross share.
// TODO(hirono): Make the loop cancellable.
var requestDriveShare = function(index) {
if (index >= shareEntries.length)
return;
return new Promise(function(fulfill) {
chrome.fileManagerPrivate.requestDriveShare(
shareEntries[index].toURL(),
dialogResult,
function() {
// TODO(hirono): Check chrome.runtime.lastError here.
fulfill(null);
});
}).then(requestDriveShare.bind(null, index + 1));
};
return requestDriveShare(0);
});
}.bind(this)).then(function() {
// Start the pasting operation.
this.fileOperationManager_.paste(
entries, destinationEntry, toMove, taskId);
this.pendingTaskIds.splice(this.pendingTaskIds.indexOf(taskId), 1);
// Publish events for failureUrls.
for (var i = 0; i < failureUrls.length; i++) {
var fileName =
decodeURIComponent(failureUrls[i].replace(/^.+\//, ''));
var event = new Event('source-not-found');
event.fileName = fileName;
event.progressType =
toMove ? ProgressItemType.MOVE : ProgressItemType.COPY;
this.dispatchEvent(event);
}
}.bind(this)).catch(function(error) {
if (error !== 'ABORT')
console.error(error.stack ? error.stack : error);
});
return toMove ? 'move' : 'copy';
},
/**
* Preloads an image thumbnail for the specified file entry.
*
* @this {FileTransferController}
* @param {Entry} entry Entry to preload a thumbnail for.
*/
preloadThumbnailImage_: function(entry) {
var metadataPromise = new Promise(function(fulfill, reject) {
this.metadataCache_.getOne(
entry,
'thumbnail|filesystem',
function(metadata) {
if (metadata)
fulfill(metadata);
else
reject('Failed to fetch metadata.');
});
}.bind(this));
var imagePromise = metadataPromise.then(function(metadata) {
return new Promise(function(fulfill, reject) {
var loader = new ThumbnailLoader(
entry, ThumbnailLoader.LoaderType.IMAGE, metadata);
loader.loadDetachedImage(function(result) {
if (result)
fulfill(loader.getImage());
});
});
});
imagePromise.then(function(image) {
// Store the image so that we can obtain the image synchronously.
imagePromise.value = image;
}, function(error) {
console.error(error.stack || error);
});
this.preloadedThumbnailImagePromise_ = imagePromise;
},
/**
* Renders a drag-and-drop thumbnail.
*
* @this {FileTransferController}
* @return {Element} Element containing the thumbnail.
*/
renderThumbnail_: function() {
var length = this.selectedEntries_.length;
var container = this.document_.querySelector('#drag-container');
var contents = this.document_.createElement('div');
contents.className = 'drag-contents';
container.appendChild(contents);
// Option 1. Multiple selection, render only a label.
if (length > 1) {
var label = this.document_.createElement('div');
label.className = 'label';
label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
contents.appendChild(label);
return container;
}
// Option 2. Thumbnail image available, then render it without
// a label.
if (this.preloadedThumbnailImagePromise_ &&
this.preloadedThumbnailImagePromise_.value) {
var thumbnailImage = this.preloadedThumbnailImagePromise_.value;
// Resize the image to canvas.
var canvas = document.createElement('canvas');
canvas.width = FileTransferController.DRAG_THUMBNAIL_SIZE_;
canvas.height = FileTransferController.DRAG_THUMBNAIL_SIZE_;
var minScale = Math.min(
thumbnailImage.width / canvas.width,
thumbnailImage.height / canvas.height);
var srcWidth = Math.min(canvas.width * minScale, thumbnailImage.width);
var srcHeight = Math.min(canvas.height * minScale, thumbnailImage.height);
var context = canvas.getContext('2d');
context.drawImage(thumbnailImage,
(thumbnailImage.width - srcWidth) / 2,
(thumbnailImage.height - srcHeight) / 2,
srcWidth,
srcHeight,
0,
0,
canvas.width,
canvas.height);
contents.classList.add('for-image');
contents.appendChild(canvas);
return container;
}
// Option 3. Thumbnail not available. Render an icon and a label.
var entry = this.selectedEntries_[0];
var icon = this.document_.createElement('div');
icon.className = 'detail-icon';
icon.setAttribute('file-type-icon', FileType.getIcon(entry));
contents.appendChild(icon);
var label = this.document_.createElement('div');
label.className = 'label';
label.textContent = entry.name;
contents.appendChild(label);
return container;
},
/**
* @this {FileTransferController}
* @param {cr.ui.List} list Drop target list
* @param {Event} event A dragstart event of DOM.
*/
onDragStart_: function(list, event) {
// Check if a drag selection should be initiated or not.
if (list.shouldStartDragSelection(event)) {
event.preventDefault();
// If this drag operation is initiated by mouse, start selecting area.
if (!this.touching_)
this.dragSelector_.startDragSelection(list, event);
return;
}
// Nothing selected.
if (!this.selectedEntries_.length) {
event.preventDefault();
return;
}
var dt = event.dataTransfer;
var canCopy = this.canCopyOrDrag_();
var canCut = this.canCutOrDrag_();
if (canCopy || canCut) {
if (canCopy && canCut) {
this.cutOrCopy_(dt, 'all');
} else if (canCopy) {
this.cutOrCopy_(dt, 'copyLink');
} else {
this.cutOrCopy_(dt, 'move');
}
} else {
event.preventDefault();
return;
}
var dragThumbnail = this.renderThumbnail_();
dt.setDragImage(dragThumbnail, 0, 0);
window[DRAG_AND_DROP_GLOBAL_DATA] = {
sourceRootURL: dt.getData('fs/sourceRootURL'),
missingFileContents: dt.getData('fs/missingFileContents')
};
},
/**
* @this {FileTransferController}
* @param {cr.ui.List} list Drop target list.
* @param {Event} event A dragend event of DOM.
*/
onDragEnd_: function(list, event) {
// TODO(fukino): This is workaround for crbug.com/373125.
// This should be removed after the bug is fixed.
this.touching_ = false;
var container = this.document_.querySelector('#drag-container');
container.textContent = '';
this.clearDropTarget_();
delete window[DRAG_AND_DROP_GLOBAL_DATA];
},
/**
* @this {FileTransferController}
* @param {boolean} onlyIntoDirectories True if the drag is only into
* directories.
* @param {(cr.ui.List|DirectoryTree)} list Drop target list.
* @param {Event} event A dragover event of DOM.
*/
onDragOver_: function(onlyIntoDirectories, list, event) {
event.preventDefault();
var entry = this.destinationEntry_ ||
(!onlyIntoDirectories && this.currentDirectoryContentEntry);
event.dataTransfer.dropEffect = this.selectDropEffect_(event, entry);
event.preventDefault();
},
/**
* @this {FileTransferController}
* @param {(cr.ui.List|DirectoryTree)} list Drop target list.
* @param {Event} event A dragenter event of DOM.
*/
onDragEnterFileList_: function(list, event) {
event.preventDefault(); // Required to prevent the cursor flicker.
this.lastEnteredTarget_ = event.target;
var item = list.getListItemAncestor(
/** @type {HTMLElement} */ (event.target));
item = item && list.isItem(item) ? item : null;
if (item === this.dropTarget_)
return;
var entry = item && list.dataModel.item(item.listIndex);
if (entry)
this.setDropTarget_(item, event.dataTransfer, entry);
else
this.clearDropTarget_();
},
/**
* @this {FileTransferController}
* @param {DirectoryTree} tree Drop target tree.
* @param {Event} event A dragenter event of DOM.
*/
onDragEnterTree_: function(tree, event) {
event.preventDefault(); // Required to prevent the cursor flicker.
this.lastEnteredTarget_ = event.target;
var item = event.target;
while (item && !(item instanceof cr.ui.TreeItem)) {
item = item.parentNode;
}
if (item === this.dropTarget_)
return;
var entry = item && item.entry;
if (entry) {
this.setDropTarget_(item, event.dataTransfer, entry);
} else {
this.clearDropTarget_();
}
},
/**
* @this {FileTransferController}
* @param {*} list Drop target list.
* @param {Event} event A dragleave event of DOM.
*/
onDragLeave_: function(list, event) {
// If mouse moves from one element to another the 'dragenter'
// event for the new element comes before the 'dragleave' event for
// the old one. In this case event.target !== this.lastEnteredTarget_
// and handler of the 'dragenter' event has already caried of
// drop target. So event.target === this.lastEnteredTarget_
// could only be if mouse goes out of listened element.
if (event.target === this.lastEnteredTarget_) {
this.clearDropTarget_();
this.lastEnteredTarget_ = null;
}
},
/**
* @this {FileTransferController}
* @param {boolean} onlyIntoDirectories True if the drag is only into
* directories.
* @param {Event} event A dragleave event of DOM.
*/
onDrop_: function(onlyIntoDirectories, event) {
if (onlyIntoDirectories && !this.dropTarget_)
return;
var destinationEntry = this.destinationEntry_ ||
this.currentDirectoryContentEntry;
if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry))
return;
event.preventDefault();
this.paste(event.dataTransfer, destinationEntry,
this.selectDropEffect_(event, destinationEntry));
this.clearDropTarget_();
},
/**
* Sets the drop target.
*
* @this {FileTransferController}
* @param {Element} domElement Target of the drop.
* @param {DataTransfer} dataTransfer Data transfer object.
* @param {DirectoryEntry} destinationEntry Destination entry.
*/
setDropTarget_: function(domElement, dataTransfer, destinationEntry) {
if (this.dropTarget_ === domElement)
return;
// Remove the old drop target.
this.clearDropTarget_();
// Set the new drop target.
this.dropTarget_ = domElement;
if (!domElement ||
!destinationEntry.isDirectory ||
!this.canPasteOrDrop_(dataTransfer, destinationEntry)) {
return;
}
// Add accept class if the domElement can accept the drag.
domElement.classList.add('accepts');
this.destinationEntry_ = destinationEntry;
// Start timer changing the directory.
this.navigateTimer_ = setTimeout(function() {
if (domElement instanceof DirectoryItem) {
// Do custom action.
/** @type {DirectoryItem} */ (domElement).doDropTargetAction();
}
this.directoryModel_.changeDirectoryEntry(destinationEntry);
}.bind(this), 2000);
},
/**
* Handles touch start.
*/
onTouchStart_: function() {
this.touching_ = true;
},
/**
* Handles touch end.
*/
onTouchEnd_: function(event) {
// TODO(fukino): We have to check if event.touches.length be 0 to support
// multi-touch operations, but event.touches has incorrect value by a bug
// (crbug.com/373125).
// After the bug is fixed, we should check event.touches.
this.touching_ = false;
},
/**
* Clears the drop target.
* @this {FileTransferController}
*/
clearDropTarget_: function() {
if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts'))
this.dropTarget_.classList.remove('accepts');
this.dropTarget_ = null;
this.destinationEntry_ = null;
if (this.navigateTimer_ !== undefined) {
clearTimeout(this.navigateTimer_);
this.navigateTimer_ = undefined;
}
},
/**
* @this {FileTransferController}
* @return {boolean} Returns false if {@code <input type="text">} element is
* currently active. Otherwise, returns true.
*/
isDocumentWideEvent_: function() {
return this.document_.activeElement.nodeName.toLowerCase() !== 'input' ||
this.document_.activeElement.type.toLowerCase() !== 'text';
},
/**
* @this {FileTransferController}
*/
onCopy_: function(event) {
if (!this.isDocumentWideEvent_() ||
!this.canCopyOrDrag_()) {
return;
}
event.preventDefault();
this.cutOrCopy_(event.clipboardData, 'copy');
this.notify_('selection-copied');
},
/**
* @this {FileTransferController}
*/
onBeforeCopy_: function(event) {
if (!this.isDocumentWideEvent_())
return;
// queryCommandEnabled returns true if event.defaultPrevented is true.
if (this.canCopyOrDrag_())
event.preventDefault();
},
/**
* @this {FileTransferController}
* @return {boolean} Returns true if all selected files are available to be
* copied.
*/
isAllSelectedFilesAvailable_: function() {
if (!this.currentDirectoryContentEntry)
return false;
var volumeInfo = this.volumeManager_.getVolumeInfo(
this.currentDirectoryContentEntry);
if (!volumeInfo)
return false;
var isDriveOffline = this.volumeManager_.getDriveConnectionState().type ===
VolumeManagerCommon.DriveConnectionType.OFFLINE;
if (this.isOnDrive && isDriveOffline && !this.allDriveFilesAvailable)
return false;
return true;
},
/**
* @this {FileTransferController}
* @return {boolean} Returns true if some files are selected and all the file
* on drive is available to be copied. Otherwise, returns false.
*/
canCopyOrDrag_: function() {
return this.isAllSelectedFilesAvailable_() &&
this.selectedEntries_.length > 0;
},
/**
* @this {FileTransferController}
*/
onCut_: function(event) {
if (!this.isDocumentWideEvent_() ||
!this.canCutOrDrag_()) {
return;
}
event.preventDefault();
this.cutOrCopy_(event.clipboardData, 'move');
this.notify_('selection-cut');
},
/**
* @this {FileTransferController}
*/
onBeforeCut_: function(event) {
if (!this.isDocumentWideEvent_())
return;
// queryCommandEnabled returns true if event.defaultPrevented is true.
if (this.canCutOrDrag_())
event.preventDefault();
},
/**
* @this {FileTransferController}
* @return {boolean} Returns true if the current directory is not read only.
*/
canCutOrDrag_: function() {
return !this.readonly && this.selectedEntries_.length > 0;
},
/**
* @this {FileTransferController}
*/
onPaste_: function(event) {
// If the event has destDirectory property, paste files into the directory.
// This occurs when the command fires from menu item 'Paste into folder'.
var destination = event.destDirectory || this.currentDirectoryContentEntry;
// Need to update here since 'beforepaste' doesn't fire.
if (!this.isDocumentWideEvent_() ||
!this.canPasteOrDrop_(event.clipboardData, destination)) {
return;
}
event.preventDefault();
var effect = this.paste(event.clipboardData, destination);
// On cut, we clear the clipboard after the file is pasted/moved so we don't
// try to move/delete the original file again.
if (effect === 'move') {
this.simulateCommand_('cut', function(event) {
event.preventDefault();
event.clipboardData.setData('fs/clear', '');
});
}
},
/**
* @this {FileTransferController}
*/
onBeforePaste_: function(event) {
if (!this.isDocumentWideEvent_())
return;
// queryCommandEnabled returns true if event.defaultPrevented is true.
if (this.canPasteOrDrop_(event.clipboardData,
this.currentDirectoryContentEntry)) {
event.preventDefault();
}
},
/**
* @this {FileTransferController}
* @param {DataTransfer} dataTransfer Data transfer object.
* @param {DirectoryEntry} destinationEntry Destination entry.
* @return {boolean} Returns true if items stored in {@code dataTransfer} can
* be pasted to {@code destinationEntry}. Otherwise, returns false.
*/
canPasteOrDrop_: function(dataTransfer, destinationEntry) {
if (!destinationEntry)
return false;
var destinationLocationInfo =
this.volumeManager_.getLocationInfo(destinationEntry);
if (!destinationLocationInfo || destinationLocationInfo.isReadOnly)
return false;
if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') === -1)
return false; // Unsupported type of content.
// Copying between different sources requires all files to be available.
if (this.getSourceRootURL_(dataTransfer) !==
destinationLocationInfo.volumeInfo.fileSystem.root.toURL() &&
this.isMissingFileContents_(dataTransfer))
return false;
return true;
},
/**
* Execute paste command.
*
* @this {FileTransferController}
* @return {boolean} Returns true, the paste is success. Otherwise, returns
* false.
*/
queryPasteCommandEnabled: function() {
if (!this.isDocumentWideEvent_()) {
return false;
}
// HACK(serya): return this.document_.queryCommandEnabled('paste')
// should be used.
var result;
this.simulateCommand_('paste', function(event) {
result = this.canPasteOrDrop_(event.clipboardData,
this.currentDirectoryContentEntry);
}.bind(this));
return result;
},
/**
* Allows to simulate commands to get access to clipboard.
*
* @this {FileTransferController}
* @param {string} command 'copy', 'cut' or 'paste'.
* @param {function(Event)} handler Event handler.
*/
simulateCommand_: function(command, handler) {
var iframe = this.document_.querySelector('#command-dispatcher');
var doc = iframe.contentDocument;
doc.addEventListener(command, handler);
doc.execCommand(command);
doc.removeEventListener(command, handler);
},
/**
* @this {FileTransferController}
*/
onSelectionChanged_: function(event) {
var entries = this.selectedEntries_;
var files = this.selectedFileObjects_ = [];
this.preloadedThumbnailImagePromise_ = null;
var fileEntries = [];
for (var i = 0; i < entries.length; i++) {
if (entries[i].isFile)
fileEntries.push(entries[i]);
}
var containsDirectory = fileEntries.length !== entries.length;
// File object must be prepeared in advance for clipboard operations
// (copy, paste and drag). DataTransfer object closes for write after
// returning control from that handlers so they may not have
// asynchronous operations.
if (!containsDirectory) {
for (var i = 0; i < fileEntries.length; i++) {
fileEntries[i].file(function(file) { files.push(file); });
}
}
if (entries.length === 1) {
// For single selection, the dragged element is created in advance,
// otherwise an image may not be loaded at the time the 'dragstart' event
// comes.
this.preloadThumbnailImage_(entries[0]);
}
if (this.isOnDrive) {
this.allDriveFilesAvailable = false;
this.metadataCache_.get(entries, 'external', function(props) {
// We consider directories not available offline for the purposes of
// file transfer since we cannot afford to recursive traversal.
this.allDriveFilesAvailable =
!containsDirectory &&
props.filter(function(p) {
return !p.availableOffline;
}).length === 0;
// |Copy| is the only menu item affected by allDriveFilesAvailable.
// It could be open right now, update its UI.
this.copyCommand_.disabled = !this.canCopyOrDrag_();
}.bind(this));
}
},
/**
* Obains directory that is displaying now.
* @this {FileTransferController}
* @return {DirectoryEntry} Entry of directry that is displaying now.
*/
get currentDirectoryContentEntry() {
return this.directoryModel_.getCurrentDirEntry();
},
/**
* @this {FileTransferController}
* @return {boolean} True if the current directory is read only.
*/
get readonly() {
return this.directoryModel_.isReadOnly();
},
/**
* @this {FileTransferController}
* @return {boolean} True if the current directory is on Drive.
*/
get isOnDrive() {
var currentDir = this.directoryModel_.getCurrentDirEntry();
if (!currentDir)
return false;
var locationInfo = this.volumeManager_.getLocationInfo(currentDir);
if (!locationInfo)
return false;
return locationInfo.isDriveBased;
},
/**
* @this {FileTransferController}
*/
notify_: function(eventName) {
var self = this;
// Set timeout to avoid recursive events.
setTimeout(function() {
cr.dispatchSimpleEvent(self, eventName);
}, 0);
},
/**
* @this {FileTransferController}
* @return {Array.<Entry>} Array of the selected entries.
*/
get selectedEntries_() {
var list = this.directoryModel_.getFileList();
var selectedIndexes = this.directoryModel_.getFileListSelection().
selectedIndexes;
var entries = selectedIndexes.map(function(index) {
return list.item(index);
});
// TODO(serya): Diagnostics for http://crbug/129642
if (entries.indexOf(undefined) !== -1) {
var index = entries.indexOf(undefined);
entries = entries.filter(function(e) { return !!e; });
console.error('Invalid selection found: list items: ', list.length,
'wrong indexe value: ', selectedIndexes[index],
'Stack trace: ', new Error().stack);
}
return entries;
},
/**
* @param {Event} event Drag event.
* @param {DirectoryEntry} destinationEntry Destination entry.
* @this {FileTransferController}
* @return {string} Returns the appropriate drop query type ('none', 'move'
* or copy') to the current modifiers status and the destination.
*/
selectDropEffect_: function(event, destinationEntry) {
if (!destinationEntry)
return 'none';
var destinationLocationInfo =
this.volumeManager_.getLocationInfo(destinationEntry);
if (!destinationLocationInfo)
return 'none';
if (destinationLocationInfo.isReadOnly)
return 'none';
if (util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'move')) {
if (!util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'copy'))
return 'move';
// TODO(mtomasz): Use volumeId instead of comparing roots, as soon as
// volumeId gets unique.
if (this.getSourceRootURL_(event.dataTransfer) ===
destinationLocationInfo.volumeInfo.fileSystem.root.toURL() &&
!event.ctrlKey) {
return 'move';
}
if (event.shiftKey) {
return 'move';
}
}
return 'copy';
},
};