blob: 5bb661776c81d1f57c75743602b79cb4c7b94113 [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.
var MAX_DRAG_THUMBAIL_COUNT = 4;
/**
* Global (placed in the window object) variable name to hold internal
* file dragging information. Needed to show visual feedback while dragging
* since DataTransfer object is in protected state. Reachable from other
* file manager instances.
*/
var DRAG_AND_DROP_GLOBAL_DATA = '__drag_and_drop_global_data';
/**
* @constructor
* @param {function} dragNodeConstructor Constructor for draggable node.
* @param {FileCopyManager} copyManager Copy manager instance.
* @param {DirectoryModel} directoryModel Directory model instance.
*/
function FileTransferController(dragNodeConstructor,
copyManager,
directoryModel) {
this.dragNodeConstructor_ = dragNodeConstructor;
this.copyManager_ = copyManager;
this.directoryModel_ = directoryModel;
this.directoryModel_.getFileListSelection().addEventListener('change',
this.onSelectionChanged_.bind(this));
/**
* DOM elements to represent selected files in drag operation.
* @type {Array.<Element>}
* @private
*/
this.dragNodes_ = [];
/**
* File objects for seletced files.
* @type {Array.<File>}
* @private
*/
this.selectedFileObjects_ = [];
}
FileTransferController.prototype = {
__proto__: cr.EventTarget.prototype,
/**
* @param {cr.ui.List} list Items in the list will be draggable.
*/
attachDragSource: function(list) {
list.style.webkitUserDrag = 'element';
list.addEventListener('dragstart', this.onDragStart_.bind(this, list));
list.addEventListener('dragend', this.onDragEnd_.bind(this, list));
},
/**
* @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).
*/
attachDropTarget: function(list, opt_onlyIntoDirectories) {
list.addEventListener('dragover', this.onDragOver_.bind(this,
!!opt_onlyIntoDirectories, list));
list.addEventListener('dragenter', this.onDragEnterList_.bind(this, list));
list.addEventListener('dragleave', this.onDragLeave_.bind(this, list));
list.addEventListener('drop', this.onDrop_.bind(this,
!!opt_onlyIntoDirectories));
},
attachBreadcrumbsDropTarget: function(breadcrumbsController) {
var container = breadcrumbsController.getContainer();
container.addEventListener('dragover',
this.onDragOver_.bind(this, true, null));
container.addEventListener('dragenter',
this.onDragEnterBreadcrumbs_.bind(this, breadcrumbsController));
container.addEventListener('dragleave',
this.onDragLeave_.bind(this, null));
container.addEventListener('drop', this.onDrop_.bind(this, true));
},
/**
* Attach handlers of copy, cut and paste operations to the document.
* @param {HTMLDocument} doc Command dispatcher.
*/
attachCopyPasteHandlers: function(doc) {
this.document_ = doc;
doc.addEventListener('beforecopy', this.onBeforeCopy_.bind(this));
doc.addEventListener('copy', this.onCopy_.bind(this));
doc.addEventListener('beforecut', this.onBeforeCut_.bind(this));
doc.addEventListener('cut', this.onCut_.bind(this));
doc.addEventListener('beforepaste', this.onBeforePaste_.bind(this));
doc.addEventListener('paste', this.onPaste_.bind(this));
this.copyCommand_ = doc.querySelector('command#copy');
},
/**
* Write the current selection to system clipboard.
*
* @param {DataTransfer} dataTransfer DataTransfer from the event.
* @param {string} effectAllowed Value must be valid for the
* |dataTransfer.effectAllowed| property ('move', 'copy', 'copyMove').
*/
cutOrCopy_: function(dataTransfer, effectAllowed) {
var directories = [];
var files = [];
var entries = this.selectedEntries_;
for (var i = 0; i < entries.length; i++) {
(entries[i].isDirectory ? directories : files).push(entries[i].fullPath);
}
// Tag to check it's filemanager data.
dataTransfer.setData('fs/tag', 'filemanager-data');
dataTransfer.setData('fs/isOnGData', this.isOnGData);
if (this.currentDirectory)
dataTransfer.setData('fs/sourceDir', this.currentDirectory.fullPath);
dataTransfer.setData('fs/directories', directories.join('\n'));
dataTransfer.setData('fs/files', files.join('\n'));
dataTransfer.effectAllowed = effectAllowed;
dataTransfer.setData('fs/effectallowed', effectAllowed);
for (var i = 0; i < this.selectedFileObjects_.length; i++) {
dataTransfer.items.add(this.selectedFileObjects_[i]);
}
},
/**
* Extracts source root from the |dataTransfer| object.
* @param {DataTransfer} dataTransfer DataTransfer object from the event.
* @return {string} Path or empty string (if unknown).
*/
getSourceRoot_: function(dataTransfer) {
var sourceDir = dataTransfer.getData('fs/sourceDir');
if (sourceDir)
return PathUtil.getRootPath(sourceDir);
// For drive search, sourceDir will be set to null, so we should double
// check that we are not on drive.
if (dataTransfer.getData('fs/isOnGData') == 'true')
return '/' + DirectoryModel.GDATA_DIRECTORY;
// |dataTransfer| in protected mode.
if (window[DRAG_AND_DROP_GLOBAL_DATA])
return window[DRAG_AND_DROP_GLOBAL_DATA].sourceRoot;
// Dragging from other tabs/windows.
var views = chrome && chrome.extension ? chrome.extension.getViews() : [];
for (var i = 0; i < views.length; i++) {
if (views[i][DRAG_AND_DROP_GLOBAL_DATA])
return views[i][DRAG_AND_DROP_GLOBAL_DATA].sourceRoot;
}
// Unknown source.
return '';
},
/**
* Queue up a file copy operation based on the current system clipboard.
* @param {DataTransfer} dataTransfer System data transfer object.
* @param {string=} opt_destinationPath Paste destination.
* @param {string=} opt_effect Desired drop/paste effect. Could be
* 'move'|'copy' (default is copy). Ignored if conflicts with
* |dataTransfer.effectAllowed|.
* @return {string} Either "copy" or "move".
*/
paste: function(dataTransfer, opt_destinationPath, opt_effect) {
var destinationPath = opt_destinationPath ||
this.directoryModel_.getCurrentDirPath();
// effectAllowed set in copy/pase handlers stay uninitialized. DnD handlers
// work fine.
var effectAllowed = dataTransfer.effectAllowed != 'uninitialized' ?
dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed');
var toMove = effectAllowed == 'move' ||
(effectAllowed == 'copyMove' && opt_effect == 'move');
var operationInfo = {
isCut: String(toMove),
isOnGData: dataTransfer.getData('fs/isOnGData'),
sourceDir: dataTransfer.getData('fs/sourceDir'),
directories: dataTransfer.getData('fs/directories'),
files: dataTransfer.getData('fs/files')
};
if (!toMove || operationInfo.sourceDir != destinationPath) {
var targetOnGData = (PathUtil.getRootType(destinationPath) ===
RootType.GDATA);
this.copyManager_.paste(operationInfo,
destinationPath,
targetOnGData);
} else {
console.log('Ignore move into the same folder');
}
return toMove ? 'move' : 'copy';
},
onDragStart_: function(list, event) {
var dt = event.dataTransfer;
var doc = list.ownerDocument;
var container = doc.querySelector('#drag-image-container');
var length = this.dragNodes_.length;
for (var i = 0; i < length; i++) {
var listItem = this.dragNodes_[i];
listItem.selected = true;
listItem.style.zIndex = length - i;
container.appendChild(listItem);
}
dt.setDragImage(container, 0, 0);
if (this.canCopyOrDrag_(dt)) {
if (this.canCutOrDrag_(dt))
this.cutOrCopy_(dt, 'copyMove');
else
this.cutOrCopy_(dt, 'copy');
} else {
event.preventDefault();
}
window[DRAG_AND_DROP_GLOBAL_DATA] = {
sourceRoot: this.directoryModel_.getCurrentRootPath()
};
},
onDragEnd_: function(list, event) {
var doc = list.ownerDocument;
var container = doc.querySelector('#drag-image-container');
container.textContent = '';
this.setDropTarget_(null);
this.setScrollSpeed_(null, 0);
delete window[DRAG_AND_DROP_GLOBAL_DATA];
},
onDragOver_: function(onlyIntoDirectories, list, event) {
if (list) {
// Scroll the list if mouse close to the top or the bottom.
var rect = list.getBoundingClientRect();
if (event.clientY - rect.top < rect.bottom - event.clientY) {
this.setScrollSpeed_(list,
-this.calculateScrollSpeed_(event.clientY - rect.top));
} else {
this.setScrollSpeed_(list,
this.calculateScrollSpeed_(rect.bottom - event.clientY));
}
}
event.preventDefault();
var path = this.destinationPath_ ||
(!onlyIntoDirectories && this.directoryModel_.getCurrentDirPath());
event.dataTransfer.dropEffect = this.selectDropEffect_(event, path);
event.preventDefault();
},
onDragEnterList_: function(list, event) {
this.dragEnterCount_++;
var item = list.getListItemAncestor(event.target);
item = item && list.isItem(item) ? item : null;
if (item == this.dropTarget_)
return;
var entry = item && list.dataModel.item(item.listIndex);
if (entry) {
this.setDropTarget_(item, entry.isDirectory, event.dataTransfer,
entry.fullPath);
} else {
this.setDropTarget_(null);
}
},
onDragEnterBreadcrumbs_: function(breadcrumbsContainer, event) {
var path = breadcrumbsContainer.getTargetPath(event);
if (!path)
return;
this.dragEnterCount_++;
this.setDropTarget_(event.target, true, event.dataTransfer, path);
},
onDragLeave_: function(list, event) {
if (this.dragEnterCount_-- == 0)
this.setDropTarget_(null);
if (event.target == list)
this.setScrollSpeed_(list, 0);
},
onDrop_: function(onlyIntoDirectories, event) {
if (onlyIntoDirectories && !this.dropTarget_)
return;
var destinationPath = this.destinationPath_ ||
this.directoryModel_.getCurrentDirPath();
if (!this.canPasteOrDrop_(event.dataTransfer, destinationPath))
return;
event.preventDefault();
this.paste(event.dataTransfer, destinationPath,
this.selectDropEffect_(event, destinationPath));
this.setDropTarget_(null);
this.setScrollSpeed_(null, 0);
},
setDropTarget_: function(domElement, isDirectory, opt_dataTransfer,
opt_destinationPath) {
if (this.dropTarget_ == domElement)
return;
/** @type {string?} */
this.destinationPath_ = null;
if (domElement) {
if (isDirectory &&
this.canPasteOrDrop_(opt_dataTransfer, opt_destinationPath)) {
domElement.classList.add('accepts');
this.destinationPath_ = opt_destinationPath;
}
} else {
this.dragEnterCount_ = 0;
}
if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts')) {
var oldDropTarget = this.dropTarget_;
var self = this;
setTimeout(function() {
if (oldDropTarget != self.dropTarget_)
oldDropTarget.classList.remove('accepts');
}, 0);
}
this.dropTarget_ = domElement;
if (this.navigateTimer_ !== undefined) {
clearTimeout(this.navigateTimer_);
this.navigateTimer_ = undefined;
}
if (domElement && isDirectory && opt_destinationPath) {
this.navigateTimer_ = setTimeout(function() {
this.directoryModel_.changeRoot(opt_destinationPath);
}.bind(this), 2000);
}
},
isDocumentWideEvent_: function(event) {
return this.document_.activeElement.nodeName.toLowerCase() != 'input' ||
this.document_.activeElement.type.toLowerCase() != 'text';
},
onCopy_: function(event) {
if (!this.isDocumentWideEvent_(event) ||
!this.canCopyOrDrag_()) {
return;
}
event.preventDefault();
this.cutOrCopy_(event.clipboardData, 'copy');
this.notify_('selection-copied');
},
onBeforeCopy_: function(event) {
if (!this.isDocumentWideEvent_(event))
return;
// queryCommandEnabled returns true if event.returnValue is false.
event.returnValue = !this.canCopyOrDrag_();
},
canCopyOrDrag_: function() {
if (this.isOnGData &&
this.directoryModel_.isOffline() &&
!this.allGDataFilesAvailable)
return false;
return this.selectedEntries_.length > 0;
},
onCut_: function(event) {
if (!this.isDocumentWideEvent_(event) ||
!this.canCutOrDrag_()) {
return;
}
event.preventDefault();
this.cutOrCopy_(event.clipboardData, 'move');
this.notify_('selection-cut');
},
onBeforeCut_: function(event) {
if (!this.isDocumentWideEvent_(event))
return;
// queryCommandEnabled returns true if event.returnValue is false.
event.returnValue = !this.canCutOrDrag_();
},
canCutOrDrag_: function() {
return !this.readonly && this.canCopyOrDrag_();
},
onPaste_: function(event) {
// Need to update here since 'beforepaste' doesn't fire.
if (!this.isDocumentWideEvent_(event) ||
!this.canPasteOrDrop_(event.clipboardData)) {
return;
}
event.preventDefault();
var effect = this.paste(event.clipboardData);
// On cut, we clear the clipboard after the file is pasted/moved so we don't
// try to move/delete the original file again.
if (effect == 'move') {
this.simulateCommand_('cut', function(event) {
event.preventDefault();
event.clipboardData.setData('fs/clear', '');
});
}
},
onBeforePaste_: function(event) {
if (!this.isDocumentWideEvent_(event))
return;
// queryCommandEnabled returns true if event.returnValue is false.
event.returnValue = !this.canPasteOrDrop_(event.clipboardData);
},
canPasteOrDrop_: function(dataTransfer, opt_destinationPath) {
var destinationPath = opt_destinationPath ||
this.directoryModel_.getCurrentDirPath();
if (this.directoryModel_.isPathReadOnly(destinationPath)) {
return false;
}
if (this.directoryModel_.isSearching())
return false;
if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') == -1)
return false; // Unsupported type of content.
if (dataTransfer.getData('fs/tag') == '') {
// Data protected. Other checks are not possible but it makes sense to
// let the user try.
return true;
}
var directories = dataTransfer.getData('fs/directories').split('\n').
filter(function(d) { return d != ''; });
for (var i = 0; i < directories.length; i++) {
if (destinationPath.substr(0, directories[i].length) == directories[i])
return false; // recursive paste.
}
return true;
},
queryPasteCommandEnabled: function() {
if (!this.isDocumentWideEvent_()) {
return false;
}
// HACK(serya): return this.document_.queryCommandEnabled('paste')
// should be used.
var result;
this.simulateCommand_('paste', function(event) {
result = this.canPasteOrDrop_(event.clipboardData);
}.bind(this));
return result;
},
/**
* Allows to simulate commands to get access to clipboard.
* @param {string} command 'copy', 'cut' or 'paste'.
* @param {Function} handler Event handler.
*/
simulateCommand_: function(command, handler) {
var iframe = this.document_.querySelector('#command-dispatcher');
var doc = iframe.contentDocument;
doc.addEventListener(command, handler);
doc.execCommand(command);
doc.removeEventListener(command, handler);
},
onSelectionChanged_: function(event) {
var entries = this.selectedEntries_;
var dragNodes = this.dragNodes_ = [];
var files = this.selectedFileObjects_ = [];
var fileEntries = [];
for (var i = 0; i < entries.length; i++) {
if (entries[i].isFile)
fileEntries.push(entries[i]);
// Items to drag are created in advance. Images must be loaded
// at the time the 'dragstart' event comes. Otherwise draggable
// image will be rendered without IMG tags.
if (dragNodes.length < MAX_DRAG_THUMBAIL_COUNT)
dragNodes.push(new this.dragNodeConstructor_(entries[i]));
}
// 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.
function prepareFileObjects() {
for (var i = 0; i < fileEntries.length; i++) {
fileEntries[i].file(function(file) { files.push(file); });
}
};
if (this.isOnGData) {
this.allGDataFilesAvailable = false;
var urls = entries.map(function(e) { return e.toURL() });
this.directoryModel_.getMetadataCache().get(
urls, 'gdata', function(props) {
// We consider directories not available offline for the purposes of
// file transfer since we cannot afford to recursive traversal.
this.allGDataFilesAvailable =
entries.filter(function(e) {return e.isDirectory}).length == 0 &&
props.filter(function(p) {return !p.availableOffline}).length == 0;
// |Copy| is the only menu item affected by allGDataFilesAvailable.
// It could be open right now, update its UI.
this.copyCommand_.disabled = !this.canCopyOrDrag_();
if (this.allGDataFilesAvailable)
prepareFileObjects();
}.bind(this));
} else {
prepareFileObjects();
}
},
get currentDirectory() {
if (this.directoryModel_.isSearching() && this.isOnGData)
return null;
return this.directoryModel_.getCurrentDirEntry();
},
get readonly() {
return this.directoryModel_.isReadOnly();
},
get isOnGData() {
return this.directoryModel_.getCurrentRootType() === RootType.GDATA;
},
notify_: function(eventName) {
var self = this;
// Set timeout to avoid recursive events.
setTimeout(function() {
cr.dispatchSimpleEvent(self, eventName);
}, 0);
},
/**
* @type {Array.<Entry>}
*/
get selectedEntries_() {
var list = this.directoryModel_.getFileList();
var selectedIndexes = this.directoryModel_.getFileListSelection().
selectedIndexes;
var entries = selectedIndexes.map(function(index) {
return list.item(index);
});
// TODO(serya): Diagnostics for http://crbug/129642
if (entries.indexOf(undefined) != -1) {
var index = entries.indexOf(undefined);
entries = entries.filter(function(e) { return !!e; });
console.error('Invalid selection found: list items: ', list.length,
'wrong indexe value: ', selectedIndexes[index],
'Stack trace: ', new Error().stack);
}
return entries;
},
selectDropEffect_: function(event, destinationPath) {
if (!destinationPath ||
this.directoryModel_.isPathReadOnly(destinationPath))
return 'none';
if (event.dataTransfer.effectAllowed == 'copyMove' &&
this.getSourceRoot_(event.dataTransfer) ==
PathUtil.getRootPath(destinationPath) &&
!event.ctrlKey) {
return 'move';
}
if (event.dataTransfer.effectAllowed == 'copyMove' &&
event.shiftKey) {
return 'move';
}
return 'copy';
},
calculateScrollSpeed_: function(distance) {
var SCROLL_AREA = 25; // Pixels.
var MIN_SCROLL_SPEED = 50; // Pixels/sec.
var MAX_SCROLL_SPEED = 300; // Pixels/sec.
if (distance < 0 || distance > SCROLL_AREA)
return 0;
return MAX_SCROLL_SPEED - (MAX_SCROLL_SPEED - MIN_SCROLL_SPEED) *
(distance / SCROLL_AREA);
},
setScrollSpeed_: function(list, speed) {
var SCROLL_INTERVAL = 200; // Milliseconds.
if (speed == 0 && this.scrollInterval_) {
clearInterval(this.scrollInterval_);
this.scrollInterval_ = null;
} else if (speed != 0 && !this.scrollInterval_) {
this.scrollInterval_ = setInterval(this.scroll_.bind(this),
SCROLL_INTERVAL);
}
this.scrollStep_ = speed * SCROLL_INTERVAL / 1000;
this.scrollList_ = list;
},
scroll_: function() {
if (this.scrollList_)
this.scrollList_.scrollTop += this.scrollStep_;
}
};