| // 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); |
| }; |