| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 'use strict'; | |
| 6 | |
| 7 /** | |
| 8 * Global (placed in the window object) variable name to hold internal | |
| 9 * file dragging information. Needed to show visual feedback while dragging | |
| 10 * since DataTransfer object is in protected state. Reachable from other | |
| 11 * file manager instances. | |
| 12 */ | |
| 13 var DRAG_AND_DROP_GLOBAL_DATA = '__drag_and_drop_global_data'; | |
| 14 | |
| 15 /** | |
| 16 * @param {HTMLDocument} doc Owning document. | |
| 17 * @param {FileOperationManager} fileOperationManager File operation manager | |
| 18 * instance. | |
| 19 * @param {MetadataCache} metadataCache Metadata cache service. | |
| 20 * @param {DirectoryModel} directoryModel Directory model instance. | |
| 21 * @param {VolumeManagerWrapper} volumeManager Volume manager instance. | |
| 22 * @param {MultiProfileShareDialog} multiProfileShareDialog Share dialog to be | |
| 23 * used to share files from another profile. | |
| 24 * @constructor | |
| 25 */ | |
| 26 function FileTransferController(doc, | |
| 27 fileOperationManager, | |
| 28 metadataCache, | |
| 29 directoryModel, | |
| 30 volumeManager, | |
| 31 multiProfileShareDialog) { | |
| 32 this.document_ = doc; | |
| 33 this.fileOperationManager_ = fileOperationManager; | |
| 34 this.metadataCache_ = metadataCache; | |
| 35 this.directoryModel_ = directoryModel; | |
| 36 this.volumeManager_ = volumeManager; | |
| 37 this.multiProfileShareDialog_ = multiProfileShareDialog; | |
| 38 | |
| 39 this.directoryModel_.getFileList().addEventListener( | |
| 40 'change', function(event) { | |
| 41 if (this.directoryModel_.getFileListSelection(). | |
| 42 getIndexSelected(event.index)) { | |
| 43 this.onSelectionChanged_(); | |
| 44 } | |
| 45 }.bind(this)); | |
| 46 this.directoryModel_.getFileListSelection().addEventListener('change', | |
| 47 this.onSelectionChanged_.bind(this)); | |
| 48 | |
| 49 /** | |
| 50 * DOM element to represent selected file in drag operation. Used if only | |
| 51 * one element is selected. | |
| 52 * @type {HTMLElement} | |
| 53 * @private | |
| 54 */ | |
| 55 this.preloadedThumbnailImageNode_ = null; | |
| 56 | |
| 57 /** | |
| 58 * File objects for selected files. | |
| 59 * | |
| 60 * @type {Array.<File>} | |
| 61 * @private | |
| 62 */ | |
| 63 this.selectedFileObjects_ = []; | |
| 64 | |
| 65 /** | |
| 66 * Drag selector. | |
| 67 * @type {DragSelector} | |
| 68 * @private | |
| 69 */ | |
| 70 this.dragSelector_ = new DragSelector(); | |
| 71 | |
| 72 /** | |
| 73 * Whether a user is touching the device or not. | |
| 74 * @type {boolean} | |
| 75 * @private | |
| 76 */ | |
| 77 this.touching_ = false; | |
| 78 } | |
| 79 | |
| 80 FileTransferController.prototype = { | |
| 81 __proto__: cr.EventTarget.prototype, | |
| 82 | |
| 83 /** | |
| 84 * @this {FileTransferController} | |
| 85 * @param {cr.ui.List} list Items in the list will be draggable. | |
| 86 */ | |
| 87 attachDragSource: function(list) { | |
| 88 list.style.webkitUserDrag = 'element'; | |
| 89 list.addEventListener('dragstart', this.onDragStart_.bind(this, list)); | |
| 90 list.addEventListener('dragend', this.onDragEnd_.bind(this, list)); | |
| 91 list.addEventListener('touchstart', this.onTouchStart_.bind(this)); | |
| 92 list.addEventListener('touchend', this.onTouchEnd_.bind(this)); | |
| 93 }, | |
| 94 | |
| 95 /** | |
| 96 * @this {FileTransferController} | |
| 97 * @param {cr.ui.List} list List itself and its directory items will could | |
| 98 * be drop target. | |
| 99 * @param {boolean=} opt_onlyIntoDirectories If true only directory list | |
| 100 * items could be drop targets. Otherwise any other place of the list | |
| 101 * accetps files (putting it into the current directory). | |
| 102 */ | |
| 103 attachFileListDropTarget: function(list, opt_onlyIntoDirectories) { | |
| 104 list.addEventListener('dragover', this.onDragOver_.bind(this, | |
| 105 !!opt_onlyIntoDirectories, list)); | |
| 106 list.addEventListener('dragenter', | |
| 107 this.onDragEnterFileList_.bind(this, list)); | |
| 108 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); | |
| 109 list.addEventListener('drop', | |
| 110 this.onDrop_.bind(this, !!opt_onlyIntoDirectories)); | |
| 111 }, | |
| 112 | |
| 113 /** | |
| 114 * @this {FileTransferController} | |
| 115 * @param {DirectoryTree} tree Its sub items will could be drop target. | |
| 116 */ | |
| 117 attachTreeDropTarget: function(tree) { | |
| 118 tree.addEventListener('dragover', this.onDragOver_.bind(this, true, tree)); | |
| 119 tree.addEventListener('dragenter', this.onDragEnterTree_.bind(this, tree)); | |
| 120 tree.addEventListener('dragleave', this.onDragLeave_.bind(this, tree)); | |
| 121 tree.addEventListener('drop', this.onDrop_.bind(this, true)); | |
| 122 }, | |
| 123 | |
| 124 /** | |
| 125 * @this {FileTransferController} | |
| 126 * @param {NavigationList} tree Its sub items will could be drop target. | |
| 127 */ | |
| 128 attachNavigationListDropTarget: function(list) { | |
| 129 list.addEventListener('dragover', | |
| 130 this.onDragOver_.bind(this, true /* onlyIntoDirectories */, list)); | |
| 131 list.addEventListener('dragenter', | |
| 132 this.onDragEnterVolumesList_.bind(this, list)); | |
| 133 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); | |
| 134 list.addEventListener('drop', | |
| 135 this.onDrop_.bind(this, true /* onlyIntoDirectories */)); | |
| 136 }, | |
| 137 | |
| 138 /** | |
| 139 * Attach handlers of copy, cut and paste operations to the document. | |
| 140 * | |
| 141 * @this {FileTransferController} | |
| 142 */ | |
| 143 attachCopyPasteHandlers: function() { | |
| 144 this.document_.addEventListener('beforecopy', | |
| 145 this.onBeforeCopy_.bind(this)); | |
| 146 this.document_.addEventListener('copy', | |
| 147 this.onCopy_.bind(this)); | |
| 148 this.document_.addEventListener('beforecut', | |
| 149 this.onBeforeCut_.bind(this)); | |
| 150 this.document_.addEventListener('cut', | |
| 151 this.onCut_.bind(this)); | |
| 152 this.document_.addEventListener('beforepaste', | |
| 153 this.onBeforePaste_.bind(this)); | |
| 154 this.document_.addEventListener('paste', | |
| 155 this.onPaste_.bind(this)); | |
| 156 this.copyCommand_ = this.document_.querySelector('command#copy'); | |
| 157 }, | |
| 158 | |
| 159 /** | |
| 160 * Write the current selection to system clipboard. | |
| 161 * | |
| 162 * @this {FileTransferController} | |
| 163 * @param {DataTransfer} dataTransfer DataTransfer from the event. | |
| 164 * @param {string} effectAllowed Value must be valid for the | |
| 165 * |dataTransfer.effectAllowed| property ('move', 'copy', 'copyMove'). | |
| 166 */ | |
| 167 cutOrCopy_: function(dataTransfer, effectAllowed) { | |
| 168 // Existence of the volumeInfo is checked in canXXX methods. | |
| 169 var volumeInfo = this.volumeManager_.getVolumeInfo( | |
| 170 this.currentDirectoryContentEntry); | |
| 171 // Tag to check it's filemanager data. | |
| 172 dataTransfer.setData('fs/tag', 'filemanager-data'); | |
| 173 dataTransfer.setData('fs/sourceRootURL', | |
| 174 volumeInfo.fileSystem.root.toURL()); | |
| 175 var sourceURLs = util.entriesToURLs(this.selectedEntries_); | |
| 176 dataTransfer.setData('fs/sources', sourceURLs.join('\n')); | |
| 177 dataTransfer.effectAllowed = effectAllowed; | |
| 178 dataTransfer.setData('fs/effectallowed', effectAllowed); | |
| 179 dataTransfer.setData('fs/missingFileContents', | |
| 180 !this.isAllSelectedFilesAvailable_()); | |
| 181 | |
| 182 for (var i = 0; i < this.selectedFileObjects_.length; i++) { | |
| 183 dataTransfer.items.add(this.selectedFileObjects_[i]); | |
| 184 } | |
| 185 }, | |
| 186 | |
| 187 /** | |
| 188 * @this {FileTransferController} | |
| 189 * @return {Object.<string, string>} Drag and drop global data object. | |
| 190 */ | |
| 191 getDragAndDropGlobalData_: function() { | |
| 192 if (window[DRAG_AND_DROP_GLOBAL_DATA]) | |
| 193 return window[DRAG_AND_DROP_GLOBAL_DATA]; | |
| 194 | |
| 195 // Dragging from other tabs/windows. | |
| 196 var views = chrome && chrome.extension ? chrome.extension.getViews() : []; | |
| 197 for (var i = 0; i < views.length; i++) { | |
| 198 if (views[i][DRAG_AND_DROP_GLOBAL_DATA]) | |
| 199 return views[i][DRAG_AND_DROP_GLOBAL_DATA]; | |
| 200 } | |
| 201 return null; | |
| 202 }, | |
| 203 | |
| 204 /** | |
| 205 * Extracts source root URL from the |dataTransfer| object. | |
| 206 * | |
| 207 * @this {FileTransferController} | |
| 208 * @param {DataTransfer} dataTransfer DataTransfer object from the event. | |
| 209 * @return {string} URL or an empty string (if unknown). | |
| 210 */ | |
| 211 getSourceRootURL_: function(dataTransfer) { | |
| 212 var sourceRootURL = dataTransfer.getData('fs/sourceRootURL'); | |
| 213 if (sourceRootURL) | |
| 214 return sourceRootURL; | |
| 215 | |
| 216 // |dataTransfer| in protected mode. | |
| 217 var globalData = this.getDragAndDropGlobalData_(); | |
| 218 if (globalData) | |
| 219 return globalData.sourceRootURL; | |
| 220 | |
| 221 // Unknown source. | |
| 222 return ''; | |
| 223 }, | |
| 224 | |
| 225 /** | |
| 226 * @this {FileTransferController} | |
| 227 * @param {DataTransfer} dataTransfer DataTransfer object from the event. | |
| 228 * @return {boolean} Returns true when missing some file contents. | |
| 229 */ | |
| 230 isMissingFileContents_: function(dataTransfer) { | |
| 231 var data = dataTransfer.getData('fs/missingFileContents'); | |
| 232 if (!data) { | |
| 233 // |dataTransfer| in protected mode. | |
| 234 var globalData = this.getDragAndDropGlobalData_(); | |
| 235 if (globalData) | |
| 236 data = globalData.missingFileContents; | |
| 237 } | |
| 238 return data === 'true'; | |
| 239 }, | |
| 240 | |
| 241 /** | |
| 242 * Obtains entries that need to share with me. | |
| 243 * The method also observers child entries of the given entries. | |
| 244 * @param {Array.<Entries>} entries Entries. | |
| 245 * @return {Promise} Promise to be fulfilled with the entries that need to | |
| 246 * share. | |
| 247 */ | |
| 248 getMultiProfileShareEntries_: function(entries) { | |
| 249 // Utility function to concat arrays. | |
| 250 var concatArrays = function(arrays) { | |
| 251 return Array.prototype.concat.apply([], arrays); | |
| 252 }; | |
| 253 | |
| 254 // Call processEntry for each item of entries. | |
| 255 var processEntries = function(entries) { | |
| 256 return Promise.all(entries.map(processEntry)).then(concatArrays); | |
| 257 }; | |
| 258 | |
| 259 // Check entry type and do particular instructions. | |
| 260 var processEntry = function(entry) { | |
| 261 if (entry.isFile) { | |
| 262 // The entry is file. Obtain metadata. | |
| 263 return new Promise(function(callback) { | |
| 264 chrome.fileBrowserPrivate.getDriveEntryProperties(entry.toURL(), | |
| 265 callback); | |
| 266 }). | |
| 267 then(function(metadata) { | |
| 268 if (metadata && | |
| 269 metadata.isHosted && | |
| 270 !metadata.sharedWithMe) { | |
| 271 return [entry]; | |
| 272 } else { | |
| 273 return []; | |
| 274 } | |
| 275 }); | |
| 276 } else { | |
| 277 // The entry is directory. Check child entries. | |
| 278 return readEntries(entry.createReader()); | |
| 279 } | |
| 280 }.bind(this); | |
| 281 | |
| 282 // Read entries from DirectoryReader and call processEntries for the chunk | |
| 283 // of entries. | |
| 284 var readEntries = function(reader) { | |
| 285 return new Promise(reader.readEntries.bind(reader)).then( | |
| 286 function(entries) { | |
| 287 if (entries.length > 0) { | |
| 288 return Promise.all( | |
| 289 [processEntries(entries), readEntries(reader)]). | |
| 290 then(concatArrays); | |
| 291 } else { | |
| 292 return []; | |
| 293 } | |
| 294 }, | |
| 295 function(error) { | |
| 296 console.warn( | |
| 297 'Error happens while reading directory.', error); | |
| 298 return []; | |
| 299 }); | |
| 300 }.bind(this); | |
| 301 | |
| 302 // Filter entries that is owned by the current user, and call | |
| 303 // processEntries. | |
| 304 return processEntries(entries.filter(function(entry) { | |
| 305 // If the volumeInfo is found, the entry belongs to the current user. | |
| 306 return !this.volumeManager_.getVolumeInfo(entry); | |
| 307 }.bind(this))); | |
| 308 }, | |
| 309 | |
| 310 /** | |
| 311 * Queue up a file copy operation based on the current system clipboard. | |
| 312 * | |
| 313 * @this {FileTransferController} | |
| 314 * @param {DataTransfer} dataTransfer System data transfer object. | |
| 315 * @param {DirectoryEntry=} opt_destinationEntry Paste destination. | |
| 316 * @param {string=} opt_effect Desired drop/paste effect. Could be | |
| 317 * 'move'|'copy' (default is copy). Ignored if conflicts with | |
| 318 * |dataTransfer.effectAllowed|. | |
| 319 * @return {string} Either "copy" or "move". | |
| 320 */ | |
| 321 paste: function(dataTransfer, opt_destinationEntry, opt_effect) { | |
| 322 var sourceURLs = dataTransfer.getData('fs/sources') ? | |
| 323 dataTransfer.getData('fs/sources').split('\n') : []; | |
| 324 // effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers | |
| 325 // work fine. | |
| 326 var effectAllowed = dataTransfer.effectAllowed !== 'uninitialized' ? | |
| 327 dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed'); | |
| 328 var toMove = effectAllowed === 'move' || | |
| 329 (effectAllowed === 'copyMove' && opt_effect === 'move'); | |
| 330 var destinationEntry = | |
| 331 opt_destinationEntry || this.currentDirectoryContentEntry; | |
| 332 var entries; | |
| 333 var failureUrls; | |
| 334 | |
| 335 util.URLsToEntries(sourceURLs). | |
| 336 then(function(result) { | |
| 337 entries = result.entries; | |
| 338 failureUrls = result.failureUrls; | |
| 339 // Check if cross share is needed or not. | |
| 340 return this.getMultiProfileShareEntries_(entries); | |
| 341 }.bind(this)). | |
| 342 then(function(shareEntries) { | |
| 343 if (shareEntries.length === 0) | |
| 344 return; | |
| 345 return this.multiProfileShareDialog_.show(shareEntries.length > 1). | |
| 346 then(function(dialogResult) { | |
| 347 if (dialogResult === 'cancel') | |
| 348 return Promise.reject('ABORT'); | |
| 349 // Do cross share. | |
| 350 // TODO(hirono): Make the loop cancellable. | |
| 351 var requestDriveShare = function(index) { | |
| 352 if (index >= shareEntries.length) | |
| 353 return Promise.cast(); | |
| 354 return new Promise(function(fulfill) { | |
| 355 chrome.fileBrowserPrivate.requestDriveShare( | |
| 356 shareEntries[index].toURL(), | |
| 357 dialogResult, | |
| 358 function() { | |
| 359 // TODO(hirono): Check chrome.runtime.lastError here. | |
| 360 fulfill(); | |
| 361 }); | |
| 362 }).then(requestDriveShare.bind(null, index + 1)); | |
| 363 }; | |
| 364 return requestDriveShare(0); | |
| 365 }); | |
| 366 }.bind(this)). | |
| 367 then(function() { | |
| 368 // Start the pasting operation. | |
| 369 this.fileOperationManager_.paste( | |
| 370 entries, destinationEntry, toMove); | |
| 371 | |
| 372 // Publish events for failureUrls. | |
| 373 for (var i = 0; i < failureUrls.length; i++) { | |
| 374 var fileName = | |
| 375 decodeURIComponent(failureUrls[i].replace(/^.+\//, '')); | |
| 376 var event = new Event('source-not-found'); | |
| 377 event.fileName = fileName; | |
| 378 event.progressType = | |
| 379 toMove ? ProgressItemType.MOVE : ProgressItemType.COPY; | |
| 380 this.dispatchEvent(event); | |
| 381 } | |
| 382 }.bind(this)). | |
| 383 catch(function(error) { | |
| 384 if (error !== 'ABORT') | |
| 385 console.error(error.stack ? error.stack : error); | |
| 386 }); | |
| 387 return toMove ? 'move' : 'copy'; | |
| 388 }, | |
| 389 | |
| 390 /** | |
| 391 * Preloads an image thumbnail for the specified file entry. | |
| 392 * | |
| 393 * @this {FileTransferController} | |
| 394 * @param {Entry} entry Entry to preload a thumbnail for. | |
| 395 */ | |
| 396 preloadThumbnailImage_: function(entry) { | |
| 397 var metadataTypes = 'thumbnail|filesystem'; | |
| 398 var thumbnailContainer = this.document_.createElement('div'); | |
| 399 this.preloadedThumbnailImageNode_ = thumbnailContainer; | |
| 400 this.preloadedThumbnailImageNode_.className = 'img-container'; | |
| 401 this.metadataCache_.get( | |
| 402 entry, | |
| 403 metadataTypes, | |
| 404 function(metadata) { | |
| 405 new ThumbnailLoader(entry, | |
| 406 ThumbnailLoader.LoaderType.IMAGE, | |
| 407 metadata). | |
| 408 load(thumbnailContainer, | |
| 409 ThumbnailLoader.FillMode.FILL); | |
| 410 }.bind(this)); | |
| 411 }, | |
| 412 | |
| 413 /** | |
| 414 * Renders a drag-and-drop thumbnail. | |
| 415 * | |
| 416 * @this {FileTransferController} | |
| 417 * @return {HTMLElement} Element containing the thumbnail. | |
| 418 */ | |
| 419 renderThumbnail_: function() { | |
| 420 var length = this.selectedEntries_.length; | |
| 421 | |
| 422 var container = this.document_.querySelector('#drag-container'); | |
| 423 var contents = this.document_.createElement('div'); | |
| 424 contents.className = 'drag-contents'; | |
| 425 container.appendChild(contents); | |
| 426 | |
| 427 var thumbnailImage; | |
| 428 if (this.preloadedThumbnailImageNode_) | |
| 429 thumbnailImage = this.preloadedThumbnailImageNode_.querySelector('img'); | |
| 430 | |
| 431 // Option 1. Multiple selection, render only a label. | |
| 432 if (length > 1) { | |
| 433 var label = this.document_.createElement('div'); | |
| 434 label.className = 'label'; | |
| 435 label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length); | |
| 436 contents.appendChild(label); | |
| 437 return container; | |
| 438 } | |
| 439 | |
| 440 // Option 2. Thumbnail image available, then render it without | |
| 441 // a label. | |
| 442 if (thumbnailImage) { | |
| 443 thumbnailImage.classList.add('drag-thumbnail'); | |
| 444 contents.classList.add('for-image'); | |
| 445 contents.appendChild(this.preloadedThumbnailImageNode_); | |
| 446 return container; | |
| 447 } | |
| 448 | |
| 449 // Option 3. Thumbnail not available. Render an icon and a label. | |
| 450 var entry = this.selectedEntries_[0]; | |
| 451 var icon = this.document_.createElement('div'); | |
| 452 icon.className = 'detail-icon'; | |
| 453 icon.setAttribute('file-type-icon', FileType.getIcon(entry)); | |
| 454 contents.appendChild(icon); | |
| 455 var label = this.document_.createElement('div'); | |
| 456 label.className = 'label'; | |
| 457 label.textContent = entry.name; | |
| 458 contents.appendChild(label); | |
| 459 return container; | |
| 460 }, | |
| 461 | |
| 462 /** | |
| 463 * @this {FileTransferController} | |
| 464 * @param {cr.ui.List} list Drop target list | |
| 465 * @param {Event} event A dragstart event of DOM. | |
| 466 */ | |
| 467 onDragStart_: function(list, event) { | |
| 468 // If a user is touching, Files.app does not receive drag operations. | |
| 469 if (this.touching_) { | |
| 470 event.preventDefault(); | |
| 471 return; | |
| 472 } | |
| 473 | |
| 474 // Check if a drag selection should be initiated or not. | |
| 475 if (list.shouldStartDragSelection(event)) { | |
| 476 this.dragSelector_.startDragSelection(list, event); | |
| 477 return; | |
| 478 } | |
| 479 | |
| 480 // Nothing selected. | |
| 481 if (!this.selectedEntries_.length) { | |
| 482 event.preventDefault(); | |
| 483 return; | |
| 484 } | |
| 485 | |
| 486 var dt = event.dataTransfer; | |
| 487 var canCopy = this.canCopyOrDrag_(dt); | |
| 488 var canCut = this.canCutOrDrag_(dt); | |
| 489 if (canCopy || canCut) { | |
| 490 if (canCopy && canCut) { | |
| 491 this.cutOrCopy_(dt, 'copyMove'); | |
| 492 } else if (canCopy) { | |
| 493 this.cutOrCopy_(dt, 'copy'); | |
| 494 } else { | |
| 495 this.cutOrCopy_(dt, 'move'); | |
| 496 } | |
| 497 } else { | |
| 498 event.preventDefault(); | |
| 499 return; | |
| 500 } | |
| 501 | |
| 502 var dragThumbnail = this.renderThumbnail_(); | |
| 503 dt.setDragImage(dragThumbnail, 1000, 1000); | |
| 504 | |
| 505 window[DRAG_AND_DROP_GLOBAL_DATA] = { | |
| 506 sourceRootURL: dt.getData('fs/sourceRootURL'), | |
| 507 missingFileContents: dt.getData('fs/missingFileContents'), | |
| 508 }; | |
| 509 }, | |
| 510 | |
| 511 /** | |
| 512 * @this {FileTransferController} | |
| 513 * @param {cr.ui.List} list Drop target list. | |
| 514 * @param {Event} event A dragend event of DOM. | |
| 515 */ | |
| 516 onDragEnd_: function(list, event) { | |
| 517 var container = this.document_.querySelector('#drag-container'); | |
| 518 container.textContent = ''; | |
| 519 this.clearDropTarget_(); | |
| 520 delete window[DRAG_AND_DROP_GLOBAL_DATA]; | |
| 521 }, | |
| 522 | |
| 523 /** | |
| 524 * @this {FileTransferController} | |
| 525 * @param {boolean} onlyIntoDirectories True if the drag is only into | |
| 526 * directories. | |
| 527 * @param {cr.ui.List} list Drop target list. | |
| 528 * @param {Event} event A dragover event of DOM. | |
| 529 */ | |
| 530 onDragOver_: function(onlyIntoDirectories, list, event) { | |
| 531 event.preventDefault(); | |
| 532 var entry = this.destinationEntry_ || | |
| 533 (!onlyIntoDirectories && this.currentDirectoryContentEntry); | |
| 534 event.dataTransfer.dropEffect = this.selectDropEffect_(event, entry); | |
| 535 event.preventDefault(); | |
| 536 }, | |
| 537 | |
| 538 /** | |
| 539 * @this {FileTransferController} | |
| 540 * @param {cr.ui.List} list Drop target list. | |
| 541 * @param {Event} event A dragenter event of DOM. | |
| 542 */ | |
| 543 onDragEnterFileList_: function(list, event) { | |
| 544 event.preventDefault(); // Required to prevent the cursor flicker. | |
| 545 this.lastEnteredTarget_ = event.target; | |
| 546 var item = list.getListItemAncestor(event.target); | |
| 547 item = item && list.isItem(item) ? item : null; | |
| 548 if (item === this.dropTarget_) | |
| 549 return; | |
| 550 | |
| 551 var entry = item && list.dataModel.item(item.listIndex); | |
| 552 if (entry) | |
| 553 this.setDropTarget_(item, event.dataTransfer, entry); | |
| 554 else | |
| 555 this.clearDropTarget_(); | |
| 556 }, | |
| 557 | |
| 558 /** | |
| 559 * @this {FileTransferController} | |
| 560 * @param {DirectoryTree} tree Drop target tree. | |
| 561 * @param {Event} event A dragenter event of DOM. | |
| 562 */ | |
| 563 onDragEnterTree_: function(tree, event) { | |
| 564 event.preventDefault(); // Required to prevent the cursor flicker. | |
| 565 this.lastEnteredTarget_ = event.target; | |
| 566 var item = event.target; | |
| 567 while (item && !(item instanceof DirectoryItem)) { | |
| 568 item = item.parentNode; | |
| 569 } | |
| 570 | |
| 571 if (item === this.dropTarget_) | |
| 572 return; | |
| 573 | |
| 574 var entry = item && item.entry; | |
| 575 if (entry) { | |
| 576 this.setDropTarget_(item, event.dataTransfer, entry); | |
| 577 } else { | |
| 578 this.clearDropTarget_(); | |
| 579 } | |
| 580 }, | |
| 581 | |
| 582 /** | |
| 583 * @this {FileTransferController} | |
| 584 * @param {NavigationList} list Drop target list. | |
| 585 * @param {Event} event A dragenter event of DOM. | |
| 586 */ | |
| 587 onDragEnterVolumesList_: function(list, event) { | |
| 588 event.preventDefault(); // Required to prevent the cursor flicker. | |
| 589 | |
| 590 this.lastEnteredTarget_ = event.target; | |
| 591 var item = list.getListItemAncestor(event.target); | |
| 592 item = item && list.isItem(item) ? item : null; | |
| 593 if (item === this.dropTarget_) | |
| 594 return; | |
| 595 | |
| 596 var modelItem = item && list.dataModel.item(item.listIndex); | |
| 597 if (modelItem && modelItem.isShortcut) { | |
| 598 this.setDropTarget_(item, event.dataTransfer, modelItem.entry); | |
| 599 return; | |
| 600 } | |
| 601 if (modelItem && modelItem.isVolume && modelItem.volumeInfo.displayRoot) { | |
| 602 this.setDropTarget_( | |
| 603 item, event.dataTransfer, modelItem.volumeInfo.displayRoot); | |
| 604 return; | |
| 605 } | |
| 606 | |
| 607 this.clearDropTarget_(); | |
| 608 }, | |
| 609 | |
| 610 /** | |
| 611 * @this {FileTransferController} | |
| 612 * @param {cr.ui.List} list Drop target list. | |
| 613 * @param {Event} event A dragleave event of DOM. | |
| 614 */ | |
| 615 onDragLeave_: function(list, event) { | |
| 616 // If mouse moves from one element to another the 'dragenter' | |
| 617 // event for the new element comes before the 'dragleave' event for | |
| 618 // the old one. In this case event.target !== this.lastEnteredTarget_ | |
| 619 // and handler of the 'dragenter' event has already caried of | |
| 620 // drop target. So event.target === this.lastEnteredTarget_ | |
| 621 // could only be if mouse goes out of listened element. | |
| 622 if (event.target === this.lastEnteredTarget_) { | |
| 623 this.clearDropTarget_(); | |
| 624 this.lastEnteredTarget_ = null; | |
| 625 } | |
| 626 }, | |
| 627 | |
| 628 /** | |
| 629 * @this {FileTransferController} | |
| 630 * @param {boolean} onlyIntoDirectories True if the drag is only into | |
| 631 * directories. | |
| 632 * @param {Event} event A dragleave event of DOM. | |
| 633 */ | |
| 634 onDrop_: function(onlyIntoDirectories, event) { | |
| 635 if (onlyIntoDirectories && !this.dropTarget_) | |
| 636 return; | |
| 637 var destinationEntry = this.destinationEntry_ || | |
| 638 this.currentDirectoryContentEntry; | |
| 639 if (!this.canPasteOrDrop_(event.dataTransfer, destinationEntry)) | |
| 640 return; | |
| 641 event.preventDefault(); | |
| 642 this.paste(event.dataTransfer, destinationEntry, | |
| 643 this.selectDropEffect_(event, destinationEntry)); | |
| 644 this.clearDropTarget_(); | |
| 645 }, | |
| 646 | |
| 647 /** | |
| 648 * Sets the drop target. | |
| 649 * | |
| 650 * @this {FileTransferController} | |
| 651 * @param {Element} domElement Target of the drop. | |
| 652 * @param {DataTransfer} dataTransfer Data transfer object. | |
| 653 * @param {DirectoryEntry} destinationEntry Destination entry. | |
| 654 */ | |
| 655 setDropTarget_: function(domElement, dataTransfer, destinationEntry) { | |
| 656 if (this.dropTarget_ === domElement) | |
| 657 return; | |
| 658 | |
| 659 // Remove the old drop target. | |
| 660 this.clearDropTarget_(); | |
| 661 | |
| 662 // Set the new drop target. | |
| 663 this.dropTarget_ = domElement; | |
| 664 | |
| 665 if (!domElement || | |
| 666 !destinationEntry.isDirectory || | |
| 667 !this.canPasteOrDrop_(dataTransfer, destinationEntry)) { | |
| 668 return; | |
| 669 } | |
| 670 | |
| 671 // Add accept class if the domElement can accept the drag. | |
| 672 domElement.classList.add('accepts'); | |
| 673 this.destinationEntry_ = destinationEntry; | |
| 674 | |
| 675 // Start timer changing the directory. | |
| 676 this.navigateTimer_ = setTimeout(function() { | |
| 677 if (domElement instanceof DirectoryItem) | |
| 678 // Do custom action. | |
| 679 (/** @type {DirectoryItem} */ domElement).doDropTargetAction(); | |
| 680 this.directoryModel_.changeDirectoryEntry(destinationEntry); | |
| 681 }.bind(this), 2000); | |
| 682 }, | |
| 683 | |
| 684 /** | |
| 685 * Handles touch start. | |
| 686 */ | |
| 687 onTouchStart_: function() { | |
| 688 this.touching_ = true; | |
| 689 }, | |
| 690 | |
| 691 /** | |
| 692 * Handles touch end. | |
| 693 */ | |
| 694 onTouchEnd_: function(event) { | |
| 695 if (event.touches.length === 0) | |
| 696 this.touching_ = false; | |
| 697 }, | |
| 698 | |
| 699 /** | |
| 700 * Clears the drop target. | |
| 701 * @this {FileTransferController} | |
| 702 */ | |
| 703 clearDropTarget_: function() { | |
| 704 if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts')) | |
| 705 this.dropTarget_.classList.remove('accepts'); | |
| 706 this.dropTarget_ = null; | |
| 707 this.destinationEntry_ = null; | |
| 708 if (this.navigateTimer_ !== undefined) { | |
| 709 clearTimeout(this.navigateTimer_); | |
| 710 this.navigateTimer_ = undefined; | |
| 711 } | |
| 712 }, | |
| 713 | |
| 714 /** | |
| 715 * @this {FileTransferController} | |
| 716 * @return {boolean} Returns false if {@code <input type="text">} element is | |
| 717 * currently active. Otherwise, returns true. | |
| 718 */ | |
| 719 isDocumentWideEvent_: function() { | |
| 720 return this.document_.activeElement.nodeName.toLowerCase() !== 'input' || | |
| 721 this.document_.activeElement.type.toLowerCase() !== 'text'; | |
| 722 }, | |
| 723 | |
| 724 /** | |
| 725 * @this {FileTransferController} | |
| 726 */ | |
| 727 onCopy_: function(event) { | |
| 728 if (!this.isDocumentWideEvent_() || | |
| 729 !this.canCopyOrDrag_()) { | |
| 730 return; | |
| 731 } | |
| 732 event.preventDefault(); | |
| 733 this.cutOrCopy_(event.clipboardData, 'copy'); | |
| 734 this.notify_('selection-copied'); | |
| 735 }, | |
| 736 | |
| 737 /** | |
| 738 * @this {FileTransferController} | |
| 739 */ | |
| 740 onBeforeCopy_: function(event) { | |
| 741 if (!this.isDocumentWideEvent_()) | |
| 742 return; | |
| 743 | |
| 744 // queryCommandEnabled returns true if event.defaultPrevented is true. | |
| 745 if (this.canCopyOrDrag_()) | |
| 746 event.preventDefault(); | |
| 747 }, | |
| 748 | |
| 749 /** | |
| 750 * @this {FileTransferController} | |
| 751 * @return {boolean} Returns true if all selected files are available to be | |
| 752 * copied. | |
| 753 */ | |
| 754 isAllSelectedFilesAvailable_: function() { | |
| 755 if (!this.currentDirectoryContentEntry) | |
| 756 return false; | |
| 757 var volumeInfo = this.volumeManager_.getVolumeInfo( | |
| 758 this.currentDirectoryContentEntry); | |
| 759 if (!volumeInfo) | |
| 760 return false; | |
| 761 var isDriveOffline = this.volumeManager_.getDriveConnectionState().type === | |
| 762 util.DriveConnectionType.OFFLINE; | |
| 763 if (this.isOnDrive && | |
| 764 isDriveOffline && | |
| 765 !this.allDriveFilesAvailable) | |
| 766 return false; | |
| 767 return true; | |
| 768 }, | |
| 769 | |
| 770 /** | |
| 771 * @this {FileTransferController} | |
| 772 * @return {boolean} Returns true if some files are selected and all the file | |
| 773 * on drive is available to be copied. Otherwise, returns false. | |
| 774 */ | |
| 775 canCopyOrDrag_: function() { | |
| 776 return this.isAllSelectedFilesAvailable_() && | |
| 777 this.selectedEntries_.length > 0; | |
| 778 }, | |
| 779 | |
| 780 /** | |
| 781 * @this {FileTransferController} | |
| 782 */ | |
| 783 onCut_: function(event) { | |
| 784 if (!this.isDocumentWideEvent_() || | |
| 785 !this.canCutOrDrag_()) { | |
| 786 return; | |
| 787 } | |
| 788 event.preventDefault(); | |
| 789 this.cutOrCopy_(event.clipboardData, 'move'); | |
| 790 this.notify_('selection-cut'); | |
| 791 }, | |
| 792 | |
| 793 /** | |
| 794 * @this {FileTransferController} | |
| 795 */ | |
| 796 onBeforeCut_: function(event) { | |
| 797 if (!this.isDocumentWideEvent_()) | |
| 798 return; | |
| 799 // queryCommandEnabled returns true if event.defaultPrevented is true. | |
| 800 if (this.canCutOrDrag_()) | |
| 801 event.preventDefault(); | |
| 802 }, | |
| 803 | |
| 804 /** | |
| 805 * @this {FileTransferController} | |
| 806 * @return {boolean} Returns true if the current directory is not read only. | |
| 807 */ | |
| 808 canCutOrDrag_: function() { | |
| 809 return !this.readonly && this.selectedEntries_.length > 0; | |
| 810 }, | |
| 811 | |
| 812 /** | |
| 813 * @this {FileTransferController} | |
| 814 */ | |
| 815 onPaste_: function(event) { | |
| 816 // Need to update here since 'beforepaste' doesn't fire. | |
| 817 if (!this.isDocumentWideEvent_() || | |
| 818 !this.canPasteOrDrop_(event.clipboardData, | |
| 819 this.currentDirectoryContentEntry)) { | |
| 820 return; | |
| 821 } | |
| 822 event.preventDefault(); | |
| 823 var effect = this.paste(event.clipboardData); | |
| 824 | |
| 825 // On cut, we clear the clipboard after the file is pasted/moved so we don't | |
| 826 // try to move/delete the original file again. | |
| 827 if (effect === 'move') { | |
| 828 this.simulateCommand_('cut', function(event) { | |
| 829 event.preventDefault(); | |
| 830 event.clipboardData.setData('fs/clear', ''); | |
| 831 }); | |
| 832 } | |
| 833 }, | |
| 834 | |
| 835 /** | |
| 836 * @this {FileTransferController} | |
| 837 */ | |
| 838 onBeforePaste_: function(event) { | |
| 839 if (!this.isDocumentWideEvent_()) | |
| 840 return; | |
| 841 // queryCommandEnabled returns true if event.defaultPrevented is true. | |
| 842 if (this.canPasteOrDrop_(event.clipboardData, | |
| 843 this.currentDirectoryContentEntry)) { | |
| 844 event.preventDefault(); | |
| 845 } | |
| 846 }, | |
| 847 | |
| 848 /** | |
| 849 * @this {FileTransferController} | |
| 850 * @param {DataTransfer} dataTransfer Data transfer object. | |
| 851 * @param {DirectoryEntry} destinationEntry Destination entry. | |
| 852 * @return {boolean} Returns true if items stored in {@code dataTransfer} can | |
| 853 * be pasted to {@code destinationEntry}. Otherwise, returns false. | |
| 854 */ | |
| 855 canPasteOrDrop_: function(dataTransfer, destinationEntry) { | |
| 856 if (!destinationEntry) | |
| 857 return false; | |
| 858 var destinationLocationInfo = | |
| 859 this.volumeManager_.getLocationInfo(destinationEntry); | |
| 860 if (!destinationLocationInfo || destinationLocationInfo.isReadOnly) | |
| 861 return false; | |
| 862 if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') === -1) | |
| 863 return false; // Unsupported type of content. | |
| 864 | |
| 865 // Copying between different sources requires all files to be available. | |
| 866 if (this.getSourceRootURL_(dataTransfer) !== | |
| 867 destinationLocationInfo.volumeInfo.fileSystem.root.toURL() && | |
| 868 this.isMissingFileContents_(dataTransfer)) | |
| 869 return false; | |
| 870 | |
| 871 return true; | |
| 872 }, | |
| 873 | |
| 874 /** | |
| 875 * Execute paste command. | |
| 876 * | |
| 877 * @this {FileTransferController} | |
| 878 * @return {boolean} Returns true, the paste is success. Otherwise, returns | |
| 879 * false. | |
| 880 */ | |
| 881 queryPasteCommandEnabled: function() { | |
| 882 if (!this.isDocumentWideEvent_()) { | |
| 883 return false; | |
| 884 } | |
| 885 | |
| 886 // HACK(serya): return this.document_.queryCommandEnabled('paste') | |
| 887 // should be used. | |
| 888 var result; | |
| 889 this.simulateCommand_('paste', function(event) { | |
| 890 result = this.canPasteOrDrop_(event.clipboardData, | |
| 891 this.currentDirectoryContentEntry); | |
| 892 }.bind(this)); | |
| 893 return result; | |
| 894 }, | |
| 895 | |
| 896 /** | |
| 897 * Allows to simulate commands to get access to clipboard. | |
| 898 * | |
| 899 * @this {FileTransferController} | |
| 900 * @param {string} command 'copy', 'cut' or 'paste'. | |
| 901 * @param {function} handler Event handler. | |
| 902 */ | |
| 903 simulateCommand_: function(command, handler) { | |
| 904 var iframe = this.document_.querySelector('#command-dispatcher'); | |
| 905 var doc = iframe.contentDocument; | |
| 906 doc.addEventListener(command, handler); | |
| 907 doc.execCommand(command); | |
| 908 doc.removeEventListener(command, handler); | |
| 909 }, | |
| 910 | |
| 911 /** | |
| 912 * @this {FileTransferController} | |
| 913 */ | |
| 914 onSelectionChanged_: function(event) { | |
| 915 var entries = this.selectedEntries_; | |
| 916 var files = this.selectedFileObjects_ = []; | |
| 917 this.preloadedThumbnailImageNode_ = null; | |
| 918 | |
| 919 var fileEntries = []; | |
| 920 for (var i = 0; i < entries.length; i++) { | |
| 921 if (entries[i].isFile) | |
| 922 fileEntries.push(entries[i]); | |
| 923 } | |
| 924 | |
| 925 if (entries.length === 1) { | |
| 926 // For single selection, the dragged element is created in advance, | |
| 927 // otherwise an image may not be loaded at the time the 'dragstart' event | |
| 928 // comes. | |
| 929 this.preloadThumbnailImage_(entries[0]); | |
| 930 } | |
| 931 | |
| 932 // File object must be prepeared in advance for clipboard operations | |
| 933 // (copy, paste and drag). DataTransfer object closes for write after | |
| 934 // returning control from that handlers so they may not have | |
| 935 // asynchronous operations. | |
| 936 var prepareFileObjects = function() { | |
| 937 for (var i = 0; i < fileEntries.length; i++) { | |
| 938 fileEntries[i].file(function(file) { files.push(file); }); | |
| 939 } | |
| 940 }; | |
| 941 | |
| 942 if (this.isOnDrive) { | |
| 943 this.allDriveFilesAvailable = false; | |
| 944 this.metadataCache_.get( | |
| 945 entries, 'drive', function(props) { | |
| 946 // We consider directories not available offline for the purposes of | |
| 947 // file transfer since we cannot afford to recursive traversal. | |
| 948 this.allDriveFilesAvailable = | |
| 949 entries.filter(function(e) { | |
| 950 return e.isDirectory; | |
| 951 }).length === 0 && | |
| 952 props.filter(function(p) { | |
| 953 return !p.availableOffline; | |
| 954 }).length === 0; | |
| 955 // |Copy| is the only menu item affected by allDriveFilesAvailable. | |
| 956 // It could be open right now, update its UI. | |
| 957 this.copyCommand_.disabled = !this.canCopyOrDrag_(); | |
| 958 | |
| 959 if (this.allDriveFilesAvailable) | |
| 960 prepareFileObjects(); | |
| 961 }.bind(this)); | |
| 962 } else { | |
| 963 prepareFileObjects(); | |
| 964 } | |
| 965 }, | |
| 966 | |
| 967 /** | |
| 968 * Obains directory that is displaying now. | |
| 969 * @this {FileTransferController} | |
| 970 * @return {DirectoryEntry} Entry of directry that is displaying now. | |
| 971 */ | |
| 972 get currentDirectoryContentEntry() { | |
| 973 return this.directoryModel_.getCurrentDirEntry(); | |
| 974 }, | |
| 975 | |
| 976 /** | |
| 977 * @this {FileTransferController} | |
| 978 * @return {boolean} True if the current directory is read only. | |
| 979 */ | |
| 980 get readonly() { | |
| 981 return this.directoryModel_.isReadOnly(); | |
| 982 }, | |
| 983 | |
| 984 /** | |
| 985 * @this {FileTransferController} | |
| 986 * @return {boolean} True if the current directory is on Drive. | |
| 987 */ | |
| 988 get isOnDrive() { | |
| 989 var currentDir = this.directoryModel_.getCurrentDirEntry(); | |
| 990 if (!currentDir) | |
| 991 return false; | |
| 992 var locationInfo = this.volumeManager_.getLocationInfo(currentDir); | |
| 993 if (!locationInfo) | |
| 994 return false; | |
| 995 return locationInfo.isDriveBased; | |
| 996 }, | |
| 997 | |
| 998 /** | |
| 999 * @this {FileTransferController} | |
| 1000 */ | |
| 1001 notify_: function(eventName) { | |
| 1002 var self = this; | |
| 1003 // Set timeout to avoid recursive events. | |
| 1004 setTimeout(function() { | |
| 1005 cr.dispatchSimpleEvent(self, eventName); | |
| 1006 }, 0); | |
| 1007 }, | |
| 1008 | |
| 1009 /** | |
| 1010 * @this {FileTransferController} | |
| 1011 * @return {Array.<Entry>} Array of the selected entries. | |
| 1012 */ | |
| 1013 get selectedEntries_() { | |
| 1014 var list = this.directoryModel_.getFileList(); | |
| 1015 var selectedIndexes = this.directoryModel_.getFileListSelection(). | |
| 1016 selectedIndexes; | |
| 1017 var entries = selectedIndexes.map(function(index) { | |
| 1018 return list.item(index); | |
| 1019 }); | |
| 1020 | |
| 1021 // TODO(serya): Diagnostics for http://crbug/129642 | |
| 1022 if (entries.indexOf(undefined) !== -1) { | |
| 1023 var index = entries.indexOf(undefined); | |
| 1024 entries = entries.filter(function(e) { return !!e; }); | |
| 1025 console.error('Invalid selection found: list items: ', list.length, | |
| 1026 'wrong indexe value: ', selectedIndexes[index], | |
| 1027 'Stack trace: ', new Error().stack); | |
| 1028 } | |
| 1029 return entries; | |
| 1030 }, | |
| 1031 | |
| 1032 /** | |
| 1033 * @param {Event} event Drag event. | |
| 1034 * @param {DirectoryEntry} destinationEntry Destination entry. | |
| 1035 * @this {FileTransferController} | |
| 1036 * @return {string} Returns the appropriate drop query type ('none', 'move' | |
| 1037 * or copy') to the current modifiers status and the destination. | |
| 1038 */ | |
| 1039 selectDropEffect_: function(event, destinationEntry) { | |
| 1040 if (!destinationEntry) | |
| 1041 return 'none'; | |
| 1042 var destinationLocationInfo = | |
| 1043 this.volumeManager_.getLocationInfo(destinationEntry); | |
| 1044 if (!destinationLocationInfo) | |
| 1045 return 'none'; | |
| 1046 if (destinationLocationInfo.isReadOnly) | |
| 1047 return 'none'; | |
| 1048 if (event.dataTransfer.effectAllowed === 'move') | |
| 1049 return 'move'; | |
| 1050 // TODO(mtomasz): Use volumeId instead of comparing roots, as soon as | |
| 1051 // volumeId gets unique. | |
| 1052 if (event.dataTransfer.effectAllowed === 'copyMove' && | |
| 1053 this.getSourceRootURL_(event.dataTransfer) === | |
| 1054 destinationLocationInfo.volumeInfo.fileSystem.root.toURL() && | |
| 1055 !event.ctrlKey) { | |
| 1056 return 'move'; | |
| 1057 } | |
| 1058 if (event.dataTransfer.effectAllowed === 'copyMove' && | |
| 1059 event.shiftKey) { | |
| 1060 return 'move'; | |
| 1061 } | |
| 1062 return 'copy'; | |
| 1063 }, | |
| 1064 }; | |
| OLD | NEW |