blob: 45631a94a2585d3456c170bf6d8a3fe8594afee1 [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.
*/
const DRAG_AND_DROP_GLOBAL_DATA = '__drag_and_drop_global_data';
/**
* @typedef {{file:File, externalFileUrl:string}}
*/
let FileAsyncData;
/**
* @param {!Document} doc Owning document.
* @param {!DirectoryTree} directoryTree Directory tree.
* @param {!ListContainer} listContainer List container.
* @param {!MultiProfileShareDialog} multiProfileShareDialog Share dialog to be
* used to share files from another profile.
* @param {function(boolean, !Array<string>): !Promise<boolean>}
* confirmationCallback called when operation requires user's confirmation.
* The opeartion will be executed if the return value resolved to true.
* @param {!ProgressCenter} progressCenter To notify starting copy operation.
* @param {!FileOperationManager} fileOperationManager File operation manager
* instance.
* @param {!MetadataModel} metadataModel Metadata cache service.
* @param {!ThumbnailModel} thumbnailModel
* @param {!DirectoryModel} directoryModel Directory model instance.
* @param {!VolumeManager} volumeManager Volume manager instance.
* @param {!FileSelectionHandler} selectionHandler Selection handler.
* @param {function((!Entry|!FakeEntry)): boolean} shouldShowCommandFor
* @struct
* @constructor
*/
function FileTransferController(
doc, listContainer, directoryTree, multiProfileShareDialog,
confirmationCallback, progressCenter, fileOperationManager, metadataModel,
thumbnailModel, directoryModel, volumeManager, selectionHandler,
shouldShowCommandFor) {
/**
* @private {!Document}
* @const
*/
this.document_ = doc;
/**
* @private {!ListContainer}
* @const
*/
this.listContainer_ = listContainer;
/**
* @private {!FileOperationManager}
* @const
*/
this.fileOperationManager_ = fileOperationManager;
/**
* @private {!MetadataModel}
* @const
*/
this.metadataModel_ = metadataModel;
/**
* @private {!ThumbnailModel}
* @const
*/
this.thumbnailModel_ = thumbnailModel;
/**
* @private {!DirectoryModel}
* @const
*/
this.directoryModel_ = directoryModel;
/**
* @private {!VolumeManager}
* @const
*/
this.volumeManager_ = volumeManager;
/**
* @private {!FileSelectionHandler}
* @const
*/
this.selectionHandler_ = selectionHandler;
/**
* @private {!MultiProfileShareDialog}
* @const
*/
this.multiProfileShareDialog_ = multiProfileShareDialog;
/**
* @private {function(boolean, !Array<string>):
* Promise<boolean>}
* @const
*/
this.confirmationCallback_ = confirmationCallback;
/**
* @private {!ProgressCenter}
* @const
*/
this.progressCenter_ = progressCenter;
/**
* @private {function((!Entry|!FakeEntry)): boolean}
* @const
*/
this.shouldShowCommandFor_ = shouldShowCommandFor;
/**
* 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.
* @private {Promise}
*/
this.preloadedThumbnailImagePromise_ = null;
/**
* File objects for selected files.
*
* @private {Object<FileAsyncData>}
*/
this.selectedAsyncData_ = {};
/**
* Drag selector.
* @private {DragSelector}
*/
this.dragSelector_ = new DragSelector();
/**
* Whether a user is touching the device or not.
* @private {boolean}
*/
this.touching_ = false;
/**
* Count of the SourceNotFound error.
* @private {number}
*/
this.sourceNotFoundErrorCount_ = 0;
/**
* @private {!cr.ui.Command}
* @const
*/
this.copyCommand_ = /** @type {!cr.ui.Command} */ (
queryRequiredElement('command#copy', this.document_));
/**
* @private {!cr.ui.Command}
* @const
*/
this.cutCommand_ = /** @type {!cr.ui.Command} */ (
queryRequiredElement('command#cut', this.document_));
/**
* @private {DirectoryEntry|FilesAppDirEntry}
*/
this.destinationEntry_ = null;
/**
* @private {EventTarget}
*/
this.lastEnteredTarget_ = null;
/**
* @private {Element}
*/
this.dropTarget_ = null;
/**
* The element for showing a label while dragging files.
* @private {Element}
*/
this.dropLabel_ = null;
/**
* @private {number}
*/
this.navigateTimer_ = 0;
// Register the events.
selectionHandler.addEventListener(
FileSelectionHandler.EventType.CHANGE,
this.onFileSelectionChanged_.bind(this));
selectionHandler.addEventListener(
FileSelectionHandler.EventType.CHANGE_THROTTLED,
this.onFileSelectionChangedThrottled_.bind(this));
this.attachDragSource_(listContainer.table.list);
this.attachFileListDropTarget_(listContainer.table.list);
this.attachDragSource_(listContainer.grid);
this.attachFileListDropTarget_(listContainer.grid);
this.attachTreeDropTarget_(directoryTree);
this.attachCopyPasteHandlers_();
// Allow to drag external files to the browser window.
chrome.fileManagerPrivate.enableExternalFileScheme();
}
/**
* Size of drag thumbnail for image files.
*
* @type {number}
* @const
* @private
*/
FileTransferController.DRAG_THUMBNAIL_SIZE_ = 64;
/**
* Y coordinate of the label to describe drop action, relative to mouse cursor.
*
* @type {number}
* @const
* @private
*/
FileTransferController.DRAG_LABEL_Y_OFFSET_ = -32;
/**
* Container for defining a copy/move operation.
*
* @param {!Array<string>} sourceURLs URLs of source entries.
* @param {!DirectoryEntry} destinationEntry Destination directory.
* @param {!EntryLocation} destinationLocationInfo Location info of the
* destination directory.
* @param {boolean} isMove true if move, false if copy.
* @constructor
* @struct
*/
FileTransferController.PastePlan = function(
sourceURLs, destinationEntry, destinationLocationInfo, isMove) {
/**
* @type {!Array<string>}
* @const
*/
this.sourceURLs = sourceURLs;
/**
* @type {!DirectoryEntry}
*/
this.destinationEntry = destinationEntry;
/**
* @type {!EntryLocation}
*/
this.destinationLocationInfo = destinationLocationInfo;
/**
* @type {boolean}
* @const
*/
this.isMove = isMove;
};
/**
* Confirmation message types.
*
* @enum {string}
*/
FileTransferController.ConfirmationType = {
NONE: 'none',
MOVE_BETWEEN_TEAM_DRIVES: 'between_team_drives',
MOVE_FROM_TEAM_DRIVE_TO_OTHER: 'move_from_team_drive_to_other',
MOVE_FROM_OTHER_TO_TEAM_DRIVE: 'move_from_other_to_team_drive',
COPY_FROM_OTHER_TO_TEAM_DRIVE: 'copy_from_other_to_team_drive',
};
/**
* Obtains whether the planned operation requires user's confirmation, as well
* as its type.
*
* @param {!Array<!Entry>} sourceEntries
* @return {FileTransferController.ConfirmationType} type of the confirmation
* required for the operation. If no confirmation is needed,
* FileTransferController.ConfirmationType.NONE will be returned.
*/
FileTransferController.PastePlan.prototype.getConfirmationType = function(
sourceEntries) {
assert(sourceEntries.length != 0);
const source = {
isTeamDrive: util.isTeamDriveEntry(sourceEntries[0]),
teamDriveName: util.getTeamDriveName(sourceEntries[0])
};
const destination = {
isTeamDrive: util.isTeamDriveEntry(this.destinationEntry),
teamDriveName: util.getTeamDriveName(this.destinationEntry)
};
if (this.isMove) {
if (source.isTeamDrive) {
if (destination.isTeamDrive) {
if (source.teamDriveName == destination.teamDriveName) {
return FileTransferController.ConfirmationType.NONE;
} else {
return FileTransferController.ConfirmationType
.MOVE_BETWEEN_TEAM_DRIVES;
}
} else {
return FileTransferController.ConfirmationType
.MOVE_FROM_TEAM_DRIVE_TO_OTHER;
}
} else if (destination.isTeamDrive) {
return FileTransferController.ConfirmationType
.MOVE_FROM_OTHER_TO_TEAM_DRIVE;
}
return FileTransferController.ConfirmationType.NONE;
} else {
if (!destination.isTeamDrive) {
return FileTransferController.ConfirmationType.NONE;
}
// Copying to Team Drive.
if (!(source.isTeamDrive &&
source.teamDriveName == destination.teamDriveName)) {
// This is not a copy within the same Team Drive.
return FileTransferController.ConfirmationType
.COPY_FROM_OTHER_TO_TEAM_DRIVE;
}
return FileTransferController.ConfirmationType.NONE;
}
};
/**
* Composes a confirmation message for the given type.
*
* @param {FileTransferController.ConfirmationType} confirmationType
* @return {!Array<string>} sentences for a confirmation dialog box.
*/
FileTransferController.PastePlan.prototype.getConfirmationMessages = function(
confirmationType, sourceEntries) {
assert(sourceEntries.length != 0);
const sourceName = util.getTeamDriveName(sourceEntries[0]);
const destinationName = util.getTeamDriveName(this.destinationEntry);
switch (confirmationType) {
case FileTransferController.ConfirmationType.MOVE_BETWEEN_TEAM_DRIVES:
return [
strf('DRIVE_CONFIRM_TD_MEMBERS_LOSE_ACCESS', sourceName),
strf('DRIVE_CONFIRM_TD_MEMBERS_GAIN_ACCESS_TO_COPY', destinationName)
];
// TODO(yamaguchi): notify ownership transfer if the two Team Drives
// belong to different domains.
case FileTransferController.ConfirmationType.MOVE_FROM_TEAM_DRIVE_TO_OTHER:
return [
strf('DRIVE_CONFIRM_TD_MEMBERS_LOSE_ACCESS', sourceName)
// TODO(yamaguchi): Warn if the operation moves at least one
// directory to My Drive, as it's no undoable.
];
case FileTransferController.ConfirmationType.MOVE_FROM_OTHER_TO_TEAM_DRIVE:
return [strf('DRIVE_CONFIRM_TD_MEMBERS_GAIN_ACCESS', destinationName)];
case FileTransferController.ConfirmationType.COPY_FROM_OTHER_TO_TEAM_DRIVE:
return [strf(
'DRIVE_CONFIRM_TD_MEMBERS_GAIN_ACCESS_TO_COPY', destinationName)];
}
assertNotReached('Invalid confirmation type: ' + confirmationType);
return [];
};
/**
* Converts list of urls to list of Entries with granting R/W permissions to
* them, which is essential when pasting files from a different profile.
*
* @param {!Array<string>} urls Urls to be converted.
* @return {Promise<!Array<string>>}
*/
FileTransferController.URLsToEntriesWithAccess = urls => {
return new Promise((resolve, reject) => {
chrome.fileManagerPrivate.grantAccess(urls, resolve.bind(null, undefined));
}).then(() => {
return util.URLsToEntries(urls);
});
};
/**
* @param {!cr.ui.List} list Items in the list will be draggable.
* @private
*/
FileTransferController.prototype.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);
};
/**
* @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).
* @private
*/
FileTransferController.prototype.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));
};
/**
* @param {!DirectoryTree} tree Its sub items will could be drop target.
* @private
*/
FileTransferController.prototype.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.
* @private
*/
FileTransferController.prototype.attachCopyPasteHandlers_ = function() {
this.document_.addEventListener('beforecopy',
this.onBeforeCutOrCopy_.bind(
this, false /* not move operation */));
this.document_.addEventListener('copy',
this.onCutOrCopy_.bind(
this, false /* not move operation */));
this.document_.addEventListener('beforecut',
this.onBeforeCutOrCopy_.bind(
this, true /* move operation */));
this.document_.addEventListener('cut',
this.onCutOrCopy_.bind(
this, true /* move operation */));
this.document_.addEventListener('beforepaste',
this.onBeforePaste_.bind(this));
this.document_.addEventListener('paste',
this.onPaste_.bind(this));
};
/**
* Write the current selection to system clipboard.
*
* @param {!ClipboardData} clipboardData ClipboardData from the event.
* @param {string} effectAllowed Value must be valid for the
* |clipboardData.effectAllowed| property.
* @private
*/
FileTransferController.prototype.cutOrCopy_ = function(
clipboardData, effectAllowed) {
const currentDirEntry = this.directoryModel_.getCurrentDirEntry();
if (!currentDirEntry) {
return;
}
const volumeInfo = this.volumeManager_.getVolumeInfo(
util.isRecentRoot(currentDirEntry) ?
this.selectionHandler_.selection.entries[0] :
currentDirEntry);
if (!volumeInfo) {
return;
}
this.appendCutOrCopyInfo_(clipboardData, effectAllowed, volumeInfo,
this.selectionHandler_.selection.entries,
!this.selectionHandler_.isAvailable());
this.appendUriList_(clipboardData,
this.selectionHandler_.selection.entries);
};
/**
* Appends copy or cut information of |entries| to |clipboardData|.
* @param {!ClipboardData} clipboardData ClipboardData from the event.
* @param {string} effectAllowed Value must be valid for the
* |clipboardData.effectAllowed| property.
* @param {!VolumeInfo} sourceVolumeInfo
* @param {!Array<!Entry>} entries
* @param {boolean} missingFileContents
* @private
*/
FileTransferController.prototype.appendCutOrCopyInfo_ = (
clipboardData,
effectAllowed,
sourceVolumeInfo,
entries,
missingFileContents
) => {
// Tag to check it's filemanager data.
clipboardData.setData('fs/tag', 'filemanager-data');
clipboardData.setData('fs/sourceRootURL',
sourceVolumeInfo.fileSystem.root.toURL());
const sourceURLs = util.entriesToURLs(entries);
clipboardData.setData('fs/sources', sourceURLs.join('\n'));
clipboardData.effectAllowed = effectAllowed;
clipboardData.setData('fs/effectallowed', effectAllowed);
clipboardData.setData('fs/missingFileContents',
missingFileContents.toString());
};
/**
* Appends uri-list of |entries| to |clipboardData|.
* @param {!ClipboardData} clipboardData ClipboardData from the event.
* @param {!Array<!Entry>} entries
* @private
*/
FileTransferController.prototype.appendUriList_ = function(
clipboardData, entries) {
let externalFileUrl;
for (let i = 0; i < entries.length; i++) {
const url = entries[i].toURL();
if (!this.selectedAsyncData_[url]) {
continue;
}
if (this.selectedAsyncData_[url].file) {
clipboardData.items.add(this.selectedAsyncData_[url].file);
}
if (!externalFileUrl) {
externalFileUrl = this.selectedAsyncData_[url].externalFileUrl;
}
}
if (externalFileUrl) {
clipboardData.setData('text/uri-list', externalFileUrl);
}
};
/**
* @return {Object<string>} Drag and drop global data object.
* @private
*/
FileTransferController.prototype.getDragAndDropGlobalData_ = () => {
if (window[DRAG_AND_DROP_GLOBAL_DATA]) {
return window[DRAG_AND_DROP_GLOBAL_DATA];
}
// Dragging from other tabs/windows.
const views = chrome && chrome.extension ? chrome.extension.getViews() : [];
for (let 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 |clipboardData| or |dragAndDropData|
* object.
*
* @param {!ClipboardData} clipboardData DataTransfer object from the event.
* @param {Object<string>} dragAndDropData The drag and drop data from
* getDragAndDropGlobalData_().
* @return {string} URL or an empty string (if unknown).
* @private
*/
FileTransferController.prototype.getSourceRootURL_ = (clipboardData, dragAndDropData) => {
const sourceRootURL = clipboardData.getData('fs/sourceRootURL');
if (sourceRootURL) {
return sourceRootURL;
}
// |clipboardData| in protected mode.
if (dragAndDropData) {
return dragAndDropData.sourceRootURL;
}
// Unknown source.
return '';
};
/**
* @param {!ClipboardData} clipboardData DataTransfer object from the event.
* @return {boolean} Returns true when missing some file contents.
* @private
*/
FileTransferController.prototype.isMissingFileContents_ =
function(clipboardData) {
let data = clipboardData.getData('fs/missingFileContents');
if (!data) {
// |clipboardData| in protected mode.
const 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<Array<Entry>>} Promise to be fulfilled with the entries
* that need to share.
* @private
*/
FileTransferController.prototype.getMultiProfileShareEntries_ =
function(entries) {
// Utility function to concat arrays.
const concatArrays = arrays => {
return Array.prototype.concat.apply([], arrays);
};
// Call processEntry for each item of entries.
const processEntries = entries => {
const files = entries.filter(entry => {
return entry.isFile;
});
const dirs = entries.filter(entry => {
return !entry.isFile;
});
const 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.
const processFileEntries = entries => {
return new Promise(callback => {
// Do not use metadata cache here because the urls come from the different
// profile.
chrome.fileManagerPrivate.getEntryProperties(
entries, ['hosted', 'sharedWithMe'], callback);
}).then(metadatas => {
return entries.filter((entry, i) => {
const metadata = metadatas[i];
return metadata && metadata.hosted && !metadata.sharedWithMe;
});
});
};
// Check child entries.
const processDirectoryEntry = entry => {
return readEntries(entry.createReader());
};
// Read entries from DirectoryReader and call processEntries for the chunk
// of entries.
const readEntries = reader => {
return new Promise(reader.readEntries.bind(reader)).then(
entries => {
if (entries.length > 0) {
return Promise.all(
[processEntries(entries), readEntries(reader)]).
then(concatArrays);
} else {
return [];
}
},
error => {
console.warn(
'Error happens while reading directory.', error);
return [];
});
};
// Filter entries that is owned by the current user, and call
// processEntries.
return processEntries(entries.filter(entry => {
// If the volumeInfo is found, the entry belongs to the current user.
return !this.volumeManager_.getVolumeInfo(/** @type {!Entry} */ (entry));
}));
};
/**
* Collects parameters of paste operation by the given command and the current
* system clipboard.
*
* @return {!FileTransferController.PastePlan}
*/
FileTransferController.prototype.preparePaste = function(
clipboardData, opt_destinationEntry, opt_effect) {
const sourceURLs = clipboardData.getData('fs/sources') ?
clipboardData.getData('fs/sources').split('\n') : [];
// effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers
// work fine.
const effectAllowed = clipboardData.effectAllowed !== 'uninitialized' ?
clipboardData.effectAllowed : clipboardData.getData('fs/effectallowed');
const destinationEntry = opt_destinationEntry ||
/** @type {DirectoryEntry} */ (this.directoryModel_.getCurrentDirEntry());
const toMove = util.isDropEffectAllowed(effectAllowed, 'move') &&
(!util.isDropEffectAllowed(effectAllowed, 'copy') ||
opt_effect === 'move');
const destinationLocationInfo =
this.volumeManager_.getLocationInfo(destinationEntry);
if (!destinationLocationInfo) {
console.log(
'Failed to get destination location for ' + destinationEntry.title() +
' while attempting to paste files.');
}
return new FileTransferController.PastePlan(
sourceURLs, destinationEntry, assert(destinationLocationInfo), toMove);
};
/**
* Queue up a file copy operation based on the current system clipboard and
* drag-and-drop global object.
*
* @param {!ClipboardData} clipboardData 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
* |clipboardData.effectAllowed|.
* @return {!Promise<string>} Either "copy" or "move".
*/
FileTransferController.prototype.paste = function(
clipboardData, opt_destinationEntry, opt_effect) {
const pastePlan =
this.preparePaste(clipboardData, opt_destinationEntry, opt_effect);
return util.URLsToEntries(pastePlan.sourceURLs).then(entriesResult => {
const sourceEntries = entriesResult.entries;
const destinationEntry = pastePlan.destinationEntry;
const destinationLocationInfo =
this.volumeManager_.getLocationInfo(destinationEntry);
const destinationIsOutsideOfDrive =
VolumeManagerCommon.getVolumeTypeFromRootType(
destinationLocationInfo.rootType) !==
VolumeManagerCommon.VolumeType.DRIVE;
// Disallow transferring hosted files from Team Drives to outside of Drive.
// This is because hosted files aren't 'real' files, so it doesn't make
// sense to allow a 'local' copy (e.g. in Downloads, or on a USB), where the
// file can't be accessed offline (or necessarily accessed at all) by the
// person who tries to open it.
// In future, block this for all hosted files, regardless of their source.
// For now, to maintain backwards-compatibility, just block this for hosted
// files stored in a Team Drive.
if (sourceEntries.some(
entry =>
util.isTeamDriveEntry(entry) && FileType.isHosted(entry)) &&
destinationIsOutsideOfDrive) {
// For now, just don't execute the paste.
// TODO(sashab): Display a warning message, and disallow drag-drop
// operations.
return null;
}
if (sourceEntries.length == 0) {
// This can happen when copied files were deleted before pasting them.
// We execute the plan as-is, so as to share the post-copy logic.
// This is basically same as getting empty by filtering same-directory
// entries.
return Promise.resolve(this.executePaste(pastePlan));
}
const confirmationType = pastePlan.getConfirmationType(sourceEntries);
if (confirmationType == FileTransferController.ConfirmationType.NONE) {
return Promise.resolve(this.executePaste(pastePlan));
}
const messages =
pastePlan.getConfirmationMessages(confirmationType, sourceEntries);
this.confirmationCallback_(pastePlan.isMove, messages)
.then(userApproved => {
if (userApproved) {
this.executePaste(pastePlan);
}
});
});
};
/**
* Queue up a file copy operation.
*
* @param {FileTransferController.PastePlan} pastePlan
* @return {string} Either "copy" or "move".
*/
FileTransferController.prototype.executePaste = function(pastePlan) {
const sourceURLs = pastePlan.sourceURLs;
const toMove = pastePlan.isMove;
const destinationEntry = pastePlan.destinationEntry;
let entries = [];
let failureUrls;
let shareEntries;
const taskId = this.fileOperationManager_.generateTaskId();
FileTransferController.URLsToEntriesWithAccess(sourceURLs)
.then(/**
* @param {Object} result
*/
result => {
failureUrls = result.failureUrls;
// The promise is not rejected, so it's safe to not remove the
// early progress center item here.
return this.fileOperationManager_.filterSameDirectoryEntry(
result.entries, destinationEntry, toMove);
})
.then(/**
* @param {!Array<Entry>} filteredEntries
* @return {!Promise<Array<Entry>>}
*/
filteredEntries => {
entries = filteredEntries;
if (entries.length === 0) {
return Promise.reject('ABORT');
}
this.pendingTaskIds.push(taskId);
const 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);
})
.then(/**
* @param {Array<Entry>} inShareEntries
* @return {!Promise<Array<Entry>>|!Promise<null>}
*/
inShareEntries => {
shareEntries = inShareEntries;
if (shareEntries.length === 0) {
return Promise.resolve(null);
}
return this.multiProfileShareDialog_.showMultiProfileShareDialog(
shareEntries.length > 1);
})
.then(
/**
* @param {?string} dialogResult
* @return {!Promise<undefined>|undefined}
*/
dialogResult => {
if (dialogResult === null) {
return;
} // No dialog was shown, skip this step.
if (dialogResult === 'cancel') {
return Promise.reject('ABORT');
}
// Do cross share.
// TODO(hirono): Make the loop cancellable.
const requestDriveShare = index => {
if (index >= shareEntries.length) {
return;
}
return new Promise(fulfill => {
chrome.fileManagerPrivate.requestDriveShare(
shareEntries[index], assert(dialogResult),
() => {
// TODO(hirono): Check chrome.runtime.lastError
// here.
fulfill();
});
})
.then(requestDriveShare.bind(null, index + 1));
};
return requestDriveShare(0);
})
.then(() => {
// Start the pasting operation.
this.fileOperationManager_.paste(
entries, destinationEntry, toMove, taskId);
this.pendingTaskIds.splice(
this.pendingTaskIds.indexOf(taskId), 1);
// Publish source not found error item.
for (let i = 0; i < failureUrls.length; i++) {
const fileName =
decodeURIComponent(failureUrls[i].replace(/^.+\//, ''));
const item = new ProgressCenterItem();
item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_;
if (toMove) {
item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', fileName);
} else {
item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', fileName);
}
item.state = ProgressItemState.ERROR;
this.progressCenter_.updateItem(item);
this.sourceNotFoundErrorCount_++;
}
})
.catch(error => {
if (error !== 'ABORT') {
console.error(error.stack ? error.stack : error);
}
});
return toMove ? 'move' : 'copy';
};
/**
* Preloads an image thumbnail for the specified file entry.
*
* @param {Entry} entry Entry to preload a thumbnail for.
* @private
*/
FileTransferController.prototype.preloadThumbnailImage_ = function(entry) {
const imagePromise = this.thumbnailModel_.get([entry]).then(metadata => {
return new Promise((fulfill, reject) => {
const loader = new ThumbnailLoader(
entry, ThumbnailLoader.LoaderType.IMAGE, metadata[0]);
loader.loadDetachedImage(result => {
if (result) {
fulfill(loader.getImage());
}
});
});
});
imagePromise.then(image => {
// Store the image so that we can obtain the image synchronously.
imagePromise.value = image;
});
this.preloadedThumbnailImagePromise_ = imagePromise;
};
/**
* Renders a drag-and-drop thumbnail.
*
* @return {!Element} Element containing the thumbnail.
* @private
*/
FileTransferController.prototype.renderThumbnail_ = function() {
const length = this.selectionHandler_.selection.entries.length;
const container = this.document_.querySelector('#drag-container');
const contents = this.document_.createElement('div');
contents.className = 'drag-contents';
container.appendChild(contents);
// Option 1. Multiple selection, render only a label.
if (length > 1) {
const 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 from preloadedThumbnailImagePromise_,
// then render it without a label.
if (this.preloadedThumbnailImagePromise_ &&
this.preloadedThumbnailImagePromise_.value) {
const thumbnailImage = this.preloadedThumbnailImagePromise_.value;
// Resize the image to canvas.
const canvas = document.createElement('canvas');
canvas.width = FileTransferController.DRAG_THUMBNAIL_SIZE_;
canvas.height = FileTransferController.DRAG_THUMBNAIL_SIZE_;
const minScale = Math.min(
thumbnailImage.width / canvas.width,
thumbnailImage.height / canvas.height);
const srcWidth = Math.min(canvas.width * minScale, thumbnailImage.width);
const srcHeight = Math.min(canvas.height * minScale, thumbnailImage.height);
const 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 image available from file grid / list, render it
// without a label.
// Because of Option 1, there is only exactly one item selected.
const index = this.selectionHandler_.selection.indexes[0];
// We only need one of the thumbnails.
const thumbnail = this.listContainer_.currentView.getThumbnail(index);
if (thumbnail) {
const canvas = document.createElement('canvas');
canvas.width = FileTransferController.DRAG_THUMBNAIL_SIZE_;
canvas.height = FileTransferController.DRAG_THUMBNAIL_SIZE_;
canvas.style.backgroundImage = thumbnail.style.backgroundImage;
canvas.style.backgroundSize = 'cover';
canvas.classList.add('for-image');
contents.appendChild(canvas);
return container;
}
// Option 4. Thumbnail not available. Render an icon and a label.
const entry = this.selectionHandler_.selection.entries[0];
const icon = this.document_.createElement('div');
icon.className = 'detail-icon';
icon.setAttribute('file-type-icon', FileType.getIcon(entry));
contents.appendChild(icon);
const label = this.document_.createElement('div');
label.className = 'label';
label.textContent = entry.name;
contents.appendChild(label);
return container;
};
/**
* @param {!cr.ui.List} list Drop target list
* @param {!Event} event A dragstart event of DOM.
* @private
*/
FileTransferController.prototype.onDragStart_ = function(list, event) {
// If renaming is in progress, drag operation should be used for selecting
// substring of the text. So we don't drag files here.
if (this.listContainer_.renameInput.currentEntry) {
event.preventDefault();
return;
}
// If this drag operation is initiated by mouse, check if we should start
// selecting area.
if (!this.touching_ && list.shouldStartDragSelection(event)) {
event.preventDefault();
this.dragSelector_.startDragSelection(list, event);
return;
}
// If the drag starts outside the files list on a touch device, cancel the
// drag.
if (this.touching_ && !list.hasDragHitElement(event)) {
event.preventDefault();
list.selectionModel_.unselectAll();
return;
}
// Nothing selected.
if (!this.selectionHandler_.selection.entries.length) {
event.preventDefault();
return;
}
const dt = event.dataTransfer;
const canCopy = this.canCopyOrDrag_();
const 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;
}
const dragThumbnail = this.renderThumbnail_();
dt.setDragImage(dragThumbnail, 0, 0);
window[DRAG_AND_DROP_GLOBAL_DATA] = {
sourceRootURL: dt.getData('fs/sourceRootURL'),
missingFileContents: dt.getData('fs/missingFileContents')
};
};
/**
* @param {!cr.ui.List} list Drop target list.
* @param {!Event} event A dragend event of DOM.
* @private
*/
FileTransferController.prototype.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;
const container = this.document_.querySelector('#drag-container');
container.textContent = '';
this.clearDropTarget_();
delete window[DRAG_AND_DROP_GLOBAL_DATA];
};
/**
* @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.
* @private
*/
FileTransferController.prototype.onDragOver_ =
function(onlyIntoDirectories, list, event) {
event.preventDefault();
let entry = this.destinationEntry_;
if (!entry && !onlyIntoDirectories) {
entry = this.directoryModel_.getCurrentDirEntry();
}
const effectAndLabel =
this.selectDropEffect_(event, this.getDragAndDropGlobalData_(), entry);
event.dataTransfer.dropEffect = effectAndLabel.getDropEffect();
event.preventDefault();
const label = effectAndLabel.getLabel();
if (!this.dropLabel_) {
this.dropLabel_ = document.querySelector("div#drop-label");
}
if (label) {
this.dropLabel_.innerText = label;
this.dropLabel_.style.left = event.pageX + 'px';
this.dropLabel_.style.top =
(event.pageY + FileTransferController.DRAG_LABEL_Y_OFFSET_) + 'px';
this.dropLabel_.style.display = 'block';
} else {
this.dropLabel_.style.display = 'none';
}
};
/**
* @param {(!cr.ui.List|!DirectoryTree)} list Drop target list.
* @param {!Event} event A dragenter event of DOM.
* @private
*/
FileTransferController.prototype.onDragEnterFileList_ = function(list, event) {
event.preventDefault(); // Required to prevent the cursor flicker.
this.lastEnteredTarget_ = event.target;
let item = list.getListItemAncestor(
/** @type {HTMLElement} */ (event.target));
item = item && list.isItem(item) ? item : null;
if (item === this.dropTarget_) {
return;
}
const entry = item && list.dataModel.item(item.listIndex);
if (entry) {
this.setDropTarget_(item, event.dataTransfer, entry);
} else {
this.clearDropTarget_();
}
};
/**
* @param {!DirectoryTree} tree Drop target tree.
* @param {!Event} event A dragenter event of DOM.
* @private
*/
FileTransferController.prototype.onDragEnterTree_ = function(tree, event) {
event.preventDefault(); // Required to prevent the cursor flicker.
this.lastEnteredTarget_ = event.target;
let item = event.target;
while (item && !(item instanceof cr.ui.TreeItem)) {
item = item.parentNode;
}
if (item === this.dropTarget_) {
return;
}
const entry = item && item.entry;
if (entry) {
this.setDropTarget_(item, event.dataTransfer, entry);
} else {
this.clearDropTarget_();
}
};
/**
* @param {*} list Drop target list.
* @param {Event} event A dragleave event of DOM.
* @private
*/
FileTransferController.prototype.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;
}
if (this.dropLabel_) {
this.dropLabel_.style.display = 'none';
}
};
/**
* @param {boolean} onlyIntoDirectories True if the drag is only into
* directories.
* @param {!Event} event A dragleave event of DOM.
* @private
*/
FileTransferController.prototype.onDrop_ = function(
onlyIntoDirectories, event) {
if (onlyIntoDirectories && !this.dropTarget_) {
return;
}
const destinationEntry = this.destinationEntry_ ||
this.directoryModel_.getCurrentDirEntry();
if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry)) {
return;
}
event.preventDefault();
this.paste(
event.dataTransfer,
/** @type {DirectoryEntry} */ (destinationEntry),
this.selectDropEffect_(
event, this.getDragAndDropGlobalData_(), destinationEntry)
.getDropEffect());
this.clearDropTarget_();
};
/**
* Change to the drop target directory.
* @private
*/
FileTransferController.prototype.changeToDropTargetDirectory_ = function() {
// Do custom action.
if (this.dropTarget_ instanceof DirectoryItem) {
/** @type {DirectoryItem} */ (this.dropTarget_).doDropTargetAction();
}
this.directoryModel_.changeDirectoryEntry(assert(this.destinationEntry_));
};
/**
* Sets the drop target.
*
* @param {Element} domElement Target of the drop.
* @param {!ClipboardData} clipboardData Data transfer object.
* @param {!DirectoryEntry|!FakeEntry} destinationEntry Destination entry.
* @private
*/
FileTransferController.prototype.setDropTarget_ = function(
domElement, clipboardData, 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) {
return;
}
if (this.selectionHandler_.selection.entries.find(element => {
return util.isSameEntry(element, destinationEntry);
})) {
return;
}
// Add accept class if the domElement can accept the drag.
domElement.classList.add('accepts');
this.destinationEntry_ = destinationEntry;
// Change directory immediately for crostini, otherwise start timer.
if (destinationEntry.rootType === VolumeManagerCommon.RootType.CROSTINI) {
this.changeToDropTargetDirectory_();
} else {
this.navigateTimer_ =
setTimeout(this.changeToDropTargetDirectory_.bind(this), 2000);
}
};
/**
* Handles touch start.
* @private
*/
FileTransferController.prototype.onTouchStart_ = function() {
this.touching_ = true;
};
/**
* Handles touch end.
* @private
*/
FileTransferController.prototype.onTouchEnd_ = function() {
// 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.
* @private
*/
FileTransferController.prototype.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_ = 0;
}
};
/**
* @return {boolean} Returns false if {@code <input type="text">} element is
* currently active. Otherwise, returns true.
* @private
*/
FileTransferController.prototype.isDocumentWideEvent_ = function() {
return this.document_.activeElement.nodeName.toLowerCase() !== 'input' ||
this.document_.activeElement.type.toLowerCase() !== 'text';
};
/**
* @return {boolean} Returns true if there is a dialog showing that we should
* treat as modal, false otherwise.
* @private
*/
FileTransferController.prototype.isModalDialogBeingDisplayed_ = () => {
// Do not allow Cut or Copy if a modal dialog is being displayed.
if (document.querySelector('.cr-dialog-container.shown') !== null) {
return true;
}
return false;
};
/**
* @param {boolean} isMove True for move operation.
* @param {!Event} event
* @private
*/
FileTransferController.prototype.onCutOrCopy_ = function(isMove, event) {
if (!this.isDocumentWideEvent_() ||
!this.canCutOrCopy_(isMove)) {
return;
}
event.preventDefault();
const clipboardData = assert(event.clipboardData);
const effectAllowed = isMove ? 'move' : 'copy';
// If current focus is on DirectoryTree, write selected item of DirectoryTree
// to system clipboard.
if (document.activeElement instanceof DirectoryTree) {
this.cutOrCopyFromDirectoryTree(
document.activeElement, clipboardData, effectAllowed);
return;
}
// If current focus is not on DirectoryTree, write the current selection in
// the list to system clipboard.
this.cutOrCopy_(clipboardData, effectAllowed);
this.blinkSelection_();
};
/**
* Performs cut or copy operation dispatched from directory tree.
* @param {!DirectoryTree} directoryTree
* @param {!ClipboardData} clipboardData
* @param {string} effectAllowed
*/
FileTransferController.prototype.cutOrCopyFromDirectoryTree = function(
directoryTree, clipboardData, effectAllowed) {
const selectedItem = document.activeElement.selectedItem;
if (selectedItem === null) {
return;
}
const entry = selectedItem.entry;
const volumeInfo = this.volumeManager_.getVolumeInfo(entry);
if (!volumeInfo) {
return;
}
// When this value is false, we cannot copy between different sources.
const missingFileContents =
volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DRIVE &&
this.volumeManager_.getDriveConnectionState().type ===
VolumeManagerCommon.DriveConnectionType.OFFLINE;
this.appendCutOrCopyInfo_(clipboardData, effectAllowed, volumeInfo, [entry],
missingFileContents);
};
/**
* @param {boolean} isMove True for move operation.
* @param {!Event} event
* @private
*/
FileTransferController.prototype.onBeforeCutOrCopy_ = function(isMove, event) {
if (!this.isDocumentWideEvent_()) {
return;
}
// queryCommandEnabled returns true if event.defaultPrevented is true.
if (this.canCutOrCopy_(isMove)) {
event.preventDefault();
}
};
/**
* @param {boolean} isMove True for move operation.
* @return {boolean}
* @private
*/
FileTransferController.prototype.canCutOrCopy_ = function(isMove) {
const command = isMove ? this.cutCommand_ : this.copyCommand_;
command.setHidden(false);
if (document.activeElement instanceof DirectoryTree) {
const selectedItem = document.activeElement.selectedItem;
if (!selectedItem) {
return false;
}
if (!this.shouldShowCommandFor_(selectedItem.entry)) {
command.setHidden(true);
return false;
}
// Cut is unavailable on Team Drive roots.
if (util.isTeamDriveRoot(selectedItem.entry)) {
return false;
}
const metadata = this.metadataModel_.getCache(
[selectedItem.entry], ['canCopy', 'canDelete']);
assert(metadata.length === 1);
if (!isMove) {
return metadata[0].canCopy !== false;
}
// We need to check source volume is writable for move operation.
const volumeInfo = this.volumeManager_.getVolumeInfo(selectedItem.entry);
return !volumeInfo.isReadOnly && metadata[0].canCopy !== false &&
metadata[0].canDelete !== false;
}
if (this.isModalDialogBeingDisplayed_()) {
return false;
}
if (!this.selectionHandler_.selection.entries.every(
this.shouldShowCommandFor_)) {
command.setHidden(true);
return false;
}
return isMove ? this.canCutOrDrag_() : this.canCopyOrDrag_() ;
};
/**
* @return {boolean} Returns true if some files are selected and all the file
* on drive is available to be copied. Otherwise, returns false.
* @private
*/
FileTransferController.prototype.canCopyOrDrag_ = function() {
if (!this.selectionHandler_.isAvailable()) {
return false;
}
if (this.selectionHandler_.selection.entries.length <= 0) {
return false;
}
const entries = this.selectionHandler_.selection.entries;
for (let i = 0; i < entries.length; i++) {
if (util.isTeamDriveRoot(entries[i])) {
return false;
}
// If selected entries are not in the same directory, we can't copy them by
// a single operation at this moment.
if (i > 0 && !util.isSiblingEntry(entries[0], entries[i])) {
return false;
}
}
// Check if canCopy is true or undefined, but not false (see
// https://crbug.com/849999).
return this.metadataModel_.getCache(entries, ['canCopy'])
.every(item => item.canCopy !== false);
};
/**
* @return {boolean} Returns true if the current directory is not read only.
* @private
*/
FileTransferController.prototype.canCutOrDrag_ = function() {
if (this.directoryModel_.isReadOnly() ||
!this.selectionHandler_.isAvailable() ||
this.selectionHandler_.selection.entries.length <= 0) {
return false;
}
const entries = this.selectionHandler_.selection.entries;
// All entries need the 'canDelete' permission.
const metadata = this.metadataModel_.getCache(entries, ['canDelete']);
if (metadata.some(item => item.canDelete === false)) {
return false;
}
return true;
};
/**
* @param {!Event} event
* @private
*/
FileTransferController.prototype.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'.
const destination =
event.destDirectory || this.directoryModel_.getCurrentDirEntry();
// Need to update here since 'beforepaste' doesn't fire.
if (!this.isDocumentWideEvent_() ||
!this.canPasteOrDrop_(assert(event.clipboardData), destination)) {
return;
}
event.preventDefault();
this.paste(assert(event.clipboardData), destination).then(effect => {
// 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', event => {
event.preventDefault();
event.clipboardData.setData('fs/clear', '');
});
}
});
};
/**
* @param {!Event} event
* @private
*/
FileTransferController.prototype.onBeforePaste_ = function(event) {
if (!this.isDocumentWideEvent_()) {
return;
}
// queryCommandEnabled returns true if event.defaultPrevented is true.
if (this.canPasteOrDrop_(assert(event.clipboardData),
this.directoryModel_.getCurrentDirEntry())) {
event.preventDefault();
}
};
/**
* @param {ClipboardData} clipboardData Clipboard data object.
* @param {DirectoryEntry|FilesAppEntry} destinationEntry Destination
* entry.
* @return {boolean} Returns true if items stored in {@code clipboardData} can
* be pasted to {@code destinationEntry}. Otherwise, returns false.
* @private
*/
FileTransferController.prototype.canPasteOrDrop_ = function(
clipboardData, destinationEntry) {
if (!clipboardData) {
return false;
}
if (!destinationEntry) {
return false;
}
const destinationLocationInfo =
this.volumeManager_.getLocationInfo(destinationEntry);
if (!destinationLocationInfo || destinationLocationInfo.isReadOnly) {
return false;
}
if (destinationLocationInfo.volumeInfo &&
destinationLocationInfo.volumeInfo.error) {
return false;
}
if (!clipboardData.types || clipboardData.types.indexOf('fs/tag') === -1) {
return false; // Unsupported type of content.
}
// Copying between different sources requires all files to be available.
if (this.getSourceRootURL_(
clipboardData, this.getDragAndDropGlobalData_()) !==
destinationLocationInfo.volumeInfo.fileSystem.root.toURL() &&
this.isMissingFileContents_(clipboardData)) {
return false;
}
// Destination entry needs the 'canAddChildren' permission.
const metadata =
this.metadataModel_.getCache([destinationEntry], ['canAddChildren']);
if (metadata[0].canAddChildren === false) {
return false;
}
return true;
};
/**
* Execute paste command.
*
* @param {DirectoryEntry|FilesAppEntry} destinationEntry
* @return {boolean} Returns true, the paste is success. Otherwise, returns
* false.
*/
FileTransferController.prototype.queryPasteCommandEnabled = function(
destinationEntry) {
if (!this.isDocumentWideEvent_()) {
return false;
}
// HACK(serya): return this.document_.queryCommandEnabled('paste')
// should be used.
let result;
this.simulateCommand_('paste', event => {
result = this.canPasteOrDrop_(
assert(event.clipboardData), destinationEntry);
});
return result;
};
/**
* Allows to simulate commands to get access to clipboard.
*
* @param {string} command 'copy', 'cut' or 'paste'.
* @param {function(Event)} handler Event handler.
* @private
*/
FileTransferController.prototype.simulateCommand_ = function(command, handler) {
const iframe = this.document_.querySelector('#command-dispatcher');
const doc = iframe.contentDocument;
doc.addEventListener(command, handler);
doc.execCommand(command);
doc.removeEventListener(command, handler);
};
/**
* @private
*/
FileTransferController.prototype.onFileSelectionChanged_ = function() {
this.preloadedThumbnailImagePromise_ = null;
};
/**
* @private
*/
FileTransferController.prototype.onFileSelectionChangedThrottled_ = function() {
// Remove file objects that are no longer in the selection.
const asyncData = {};
const entries = this.selectionHandler_.selection.entries;
for (let i = 0; i < entries.length; i++) {
const entryUrl = entries[i].toURL();
if (entryUrl in this.selectedAsyncData_) {
asyncData[entryUrl] = this.selectedAsyncData_[entryUrl];
}
}
this.selectedAsyncData_ = asyncData;
const fileEntries = [];
for (let i = 0; i < entries.length; i++) {
if (entries[i].isFile) {
fileEntries.push(entries[i]);
}
if (!(entries[i].toURL() in asyncData)) {
asyncData[entries[i].toURL()] = {externalFileUrl: '', file: null};
}
}
const containsDirectory = this.selectionHandler_.selection.directoryCount > 0;
// 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 (let i = 0; i < fileEntries.length; i++) {
(fileEntry => {
if (!(asyncData[fileEntry.toURL()].file)) {
fileEntry.file(file => {
asyncData[fileEntry.toURL()].file = file;
});
}
})(fileEntries[i]);
}
}
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]);
}
this.metadataModel_
.get(entries, ['alternateUrl', 'externalFileUrl', 'hosted'])
.then(metadataList => {
// |Copy| is the only menu item affected by allDriveFilesAvailable_.
// It could be open right now, update its UI.
this.copyCommand_.disabled =
!this.canCutOrCopy_(false /* not move operation */);
for (let i = 0; i < entries.length; i++) {
if (entries[i].isFile) {
if (metadataList[i].hosted) {
asyncData[entries[i].toURL()].externalFileUrl =
metadataList[i].alternateUrl;
} else {
asyncData[entries[i].toURL()].externalFileUrl =
metadataList[i].externalFileUrl;
}
}
}
});
};
/**
* @param {!Event} event Drag event.
* @param {Object<string>} dragAndDropData drag & drop data from
* getDragAndDropGlobalData_().
* @param {DirectoryEntry|FilesAppEntry} destinationEntry Destination
* entry.
* @return {DropEffectAndLabel} Returns the appropriate drop query type
* ('none', 'move' or copy') to the current modifiers status and the
* destination, as well as label message to describe why the operation is
* not allowed.
* @private
*/
FileTransferController.prototype.selectDropEffect_ = function(
event, dragAndDropData, destinationEntry) {
if (!destinationEntry) {
return new DropEffectAndLabel(DropEffectType.NONE, null);
}
const destinationLocationInfo =
this.volumeManager_.getLocationInfo(destinationEntry);
if (!destinationLocationInfo) {
return new DropEffectAndLabel(DropEffectType.NONE, null);
}
if (destinationLocationInfo.volumeInfo &&
destinationLocationInfo.volumeInfo.error) {
return new DropEffectAndLabel(DropEffectType.NONE, null);
}
if (destinationLocationInfo.isReadOnly) {
if (destinationLocationInfo.isSpecialSearchRoot) {
// The location is a fake entry that corresponds to special search.
return new DropEffectAndLabel(DropEffectType.NONE, null);
}
if (destinationLocationInfo.rootType ==
VolumeManagerCommon.RootType.CROSTINI) {
// The location is a the fake entry for crostini. Start container.
return new DropEffectAndLabel(
DropEffectType.NONE, strf('OPENING_LINUX_FILES'));
}
if (destinationLocationInfo.volumeInfo.isReadOnlyRemovableDevice) {
return new DropEffectAndLabel(DropEffectType.NONE,
strf('DEVICE_WRITE_PROTECTED'));
}
// The disk device is not write-protected but read-only.
// Currently, the only remaining possibility is that write access to
// removable drives is restricted by device policy.
return new DropEffectAndLabel(DropEffectType.NONE,
strf('DEVICE_ACCESS_RESTRICTED'));
}
const destinationMetadata =
this.metadataModel_.getCache([destinationEntry], ['canAddChildren']);
if (destinationMetadata.length === 1 &&
destinationMetadata[0].canAddChildren === false) {
// TODO(sashab): Distinguish between copy/move operations and display
// corresponding warning text here.
return new DropEffectAndLabel(
DropEffectType.NONE,
strf('DROP_TARGET_FOLDER_NO_MOVE_PERMISSION', destinationEntry.name));
}
if (util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'move')) {
if (!util.isDropEffectAllowed(event.dataTransfer.effectAllowed, 'copy')) {
return new DropEffectAndLabel(DropEffectType.MOVE, null);
}
// TODO(mtomasz): Use volumeId instead of comparing roots, as soon as
// volumeId gets unique.
if (this.getSourceRootURL_(event.dataTransfer, dragAndDropData) ===
destinationLocationInfo.volumeInfo.fileSystem.root.toURL() &&
!event.ctrlKey) {
return new DropEffectAndLabel(DropEffectType.MOVE, null);
}
if (event.shiftKey) {
return new DropEffectAndLabel(DropEffectType.MOVE, null);
}
}
return new DropEffectAndLabel(DropEffectType.COPY, null);
};
/**
* Blinks the selection. Used to give feedback when copying or cutting the
* selection.
* @private
*/
FileTransferController.prototype.blinkSelection_ = function() {
const selection = this.selectionHandler_.selection;
if (!selection || selection.totalCount == 0) {
return;
}
const listItems = [];
for (let i = 0; i < selection.entries.length; i++) {
const selectedIndex = selection.indexes[i];
const listItem =
this.listContainer_.currentList.getListItemByIndex(selectedIndex);
if (listItem) {
listItem.classList.add('blink');
listItems.push(listItem);
}
}
setTimeout(() => {
for (let i = 0; i < listItems.length; i++) {
listItems[i].classList.remove('blink');
}
}, 100);
};