| 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 * @constructor | |
| 22 */ | |
| 23 function FileTransferController(doc, | |
| 24 fileOperationManager, | |
| 25 metadataCache, | |
| 26 directoryModel) { | |
| 27 this.document_ = doc; | |
| 28 this.fileOperationManager_ = fileOperationManager; | |
| 29 this.metadataCache_ = metadataCache; | |
| 30 this.directoryModel_ = directoryModel; | |
| 31 | |
| 32 this.directoryModel_.getFileListSelection().addEventListener('change', | |
| 33 this.onSelectionChanged_.bind(this)); | |
| 34 | |
| 35 /** | |
| 36 * DOM element to represent selected file in drag operation. Used if only | |
| 37 * one element is selected. | |
| 38 * @type {HTMLElement} | |
| 39 * @private | |
| 40 */ | |
| 41 this.preloadedThumbnailImageNode_ = null; | |
| 42 | |
| 43 /** | |
| 44 * File objects for selected files. | |
| 45 * | |
| 46 * @type {Array.<File>} | |
| 47 * @private | |
| 48 */ | |
| 49 this.selectedFileObjects_ = []; | |
| 50 | |
| 51 /** | |
| 52 * Drag selector. | |
| 53 * @type {DragSelector} | |
| 54 * @private | |
| 55 */ | |
| 56 this.dragSelector_ = new DragSelector(); | |
| 57 | |
| 58 /** | |
| 59 * Whether a user is touching the device or not. | |
| 60 * @type {boolean} | |
| 61 * @private | |
| 62 */ | |
| 63 this.touching_ = false; | |
| 64 } | |
| 65 | |
| 66 FileTransferController.prototype = { | |
| 67 __proto__: cr.EventTarget.prototype, | |
| 68 | |
| 69 /** | |
| 70 * @this {FileTransferController} | |
| 71 * @param {cr.ui.List} list Items in the list will be draggable. | |
| 72 */ | |
| 73 attachDragSource: function(list) { | |
| 74 list.style.webkitUserDrag = 'element'; | |
| 75 list.addEventListener('dragstart', this.onDragStart_.bind(this, list)); | |
| 76 list.addEventListener('dragend', this.onDragEnd_.bind(this, list)); | |
| 77 list.addEventListener('touchstart', this.onTouchStart_.bind(this)); | |
| 78 list.addEventListener('touchend', this.onTouchEnd_.bind(this)); | |
| 79 }, | |
| 80 | |
| 81 /** | |
| 82 * @this {FileTransferController} | |
| 83 * @param {cr.ui.List} list List itself and its directory items will could | |
| 84 * be drop target. | |
| 85 * @param {boolean=} opt_onlyIntoDirectories If true only directory list | |
| 86 * items could be drop targets. Otherwise any other place of the list | |
| 87 * accetps files (putting it into the current directory). | |
| 88 */ | |
| 89 attachFileListDropTarget: function(list, opt_onlyIntoDirectories) { | |
| 90 list.addEventListener('dragover', this.onDragOver_.bind(this, | |
| 91 !!opt_onlyIntoDirectories, list)); | |
| 92 list.addEventListener('dragenter', | |
| 93 this.onDragEnterFileList_.bind(this, list)); | |
| 94 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); | |
| 95 list.addEventListener('drop', | |
| 96 this.onDrop_.bind(this, !!opt_onlyIntoDirectories)); | |
| 97 }, | |
| 98 | |
| 99 /** | |
| 100 * @this {FileTransferController} | |
| 101 * @param {DirectoryTree} tree Its sub items will could be drop target. | |
| 102 */ | |
| 103 attachTreeDropTarget: function(tree) { | |
| 104 tree.addEventListener('dragover', this.onDragOver_.bind(this, true, tree)); | |
| 105 tree.addEventListener('dragenter', this.onDragEnterTree_.bind(this, tree)); | |
| 106 tree.addEventListener('dragleave', this.onDragLeave_.bind(this, tree)); | |
| 107 tree.addEventListener('drop', this.onDrop_.bind(this, true)); | |
| 108 }, | |
| 109 | |
| 110 /** | |
| 111 * @this {FileTransferController} | |
| 112 * @param {NavigationList} tree Its sub items will could be drop target. | |
| 113 */ | |
| 114 attachNavigationListDropTarget: function(list) { | |
| 115 list.addEventListener('dragover', | |
| 116 this.onDragOver_.bind(this, true /* onlyIntoDirectories */, list)); | |
| 117 list.addEventListener('dragenter', | |
| 118 this.onDragEnterVolumesList_.bind(this, list)); | |
| 119 list.addEventListener('dragleave', this.onDragLeave_.bind(this, list)); | |
| 120 list.addEventListener('drop', | |
| 121 this.onDrop_.bind(this, true /* onlyIntoDirectories */)); | |
| 122 }, | |
| 123 | |
| 124 /** | |
| 125 * Attach handlers of copy, cut and paste operations to the document. | |
| 126 * | |
| 127 * @this {FileTransferController} | |
| 128 */ | |
| 129 attachCopyPasteHandlers: function() { | |
| 130 this.document_.addEventListener('beforecopy', | |
| 131 this.onBeforeCopy_.bind(this)); | |
| 132 this.document_.addEventListener('copy', | |
| 133 this.onCopy_.bind(this)); | |
| 134 this.document_.addEventListener('beforecut', | |
| 135 this.onBeforeCut_.bind(this)); | |
| 136 this.document_.addEventListener('cut', | |
| 137 this.onCut_.bind(this)); | |
| 138 this.document_.addEventListener('beforepaste', | |
| 139 this.onBeforePaste_.bind(this)); | |
| 140 this.document_.addEventListener('paste', | |
| 141 this.onPaste_.bind(this)); | |
| 142 this.copyCommand_ = this.document_.querySelector('command#copy'); | |
| 143 }, | |
| 144 | |
| 145 /** | |
| 146 * Write the current selection to system clipboard. | |
| 147 * | |
| 148 * @this {FileTransferController} | |
| 149 * @param {DataTransfer} dataTransfer DataTransfer from the event. | |
| 150 * @param {string} effectAllowed Value must be valid for the | |
| 151 * |dataTransfer.effectAllowed| property ('move', 'copy', 'copyMove'). | |
| 152 */ | |
| 153 cutOrCopy_: function(dataTransfer, effectAllowed) { | |
| 154 // Tag to check it's filemanager data. | |
| 155 dataTransfer.setData('fs/tag', 'filemanager-data'); | |
| 156 dataTransfer.setData('fs/sourceRoot', | |
| 157 this.directoryModel_.getCurrentRootPath()); | |
| 158 var sourcePaths = | |
| 159 this.selectedEntries_.map(function(e) { return e.fullPath; }); | |
| 160 dataTransfer.setData('fs/sources', sourcePaths.join('\n')); | |
| 161 dataTransfer.effectAllowed = effectAllowed; | |
| 162 dataTransfer.setData('fs/effectallowed', effectAllowed); | |
| 163 | |
| 164 for (var i = 0; i < this.selectedFileObjects_.length; i++) { | |
| 165 dataTransfer.items.add(this.selectedFileObjects_[i]); | |
| 166 } | |
| 167 }, | |
| 168 | |
| 169 /** | |
| 170 * Extracts source root from the |dataTransfer| object. | |
| 171 * | |
| 172 * @this {FileTransferController} | |
| 173 * @param {DataTransfer} dataTransfer DataTransfer object from the event. | |
| 174 * @return {string} Path or empty string (if unknown). | |
| 175 */ | |
| 176 getSourceRoot_: function(dataTransfer) { | |
| 177 var sourceRoot = dataTransfer.getData('fs/sourceRoot'); | |
| 178 if (sourceRoot) | |
| 179 return sourceRoot; | |
| 180 | |
| 181 // |dataTransfer| in protected mode. | |
| 182 if (window[DRAG_AND_DROP_GLOBAL_DATA]) | |
| 183 return window[DRAG_AND_DROP_GLOBAL_DATA].sourceRoot; | |
| 184 | |
| 185 // Dragging from other tabs/windows. | |
| 186 var views = chrome && chrome.extension ? chrome.extension.getViews() : []; | |
| 187 for (var i = 0; i < views.length; i++) { | |
| 188 if (views[i][DRAG_AND_DROP_GLOBAL_DATA]) | |
| 189 return views[i][DRAG_AND_DROP_GLOBAL_DATA].sourceRoot; | |
| 190 } | |
| 191 | |
| 192 // Unknown source. | |
| 193 return ''; | |
| 194 }, | |
| 195 | |
| 196 /** | |
| 197 * Queue up a file copy operation based on the current system clipboard. | |
| 198 * | |
| 199 * @this {FileTransferController} | |
| 200 * @param {DataTransfer} dataTransfer System data transfer object. | |
| 201 * @param {string=} opt_destinationPath Paste destination. | |
| 202 * @param {string=} opt_effect Desired drop/paste effect. Could be | |
| 203 * 'move'|'copy' (default is copy). Ignored if conflicts with | |
| 204 * |dataTransfer.effectAllowed|. | |
| 205 * @return {string} Either "copy" or "move". | |
| 206 */ | |
| 207 paste: function(dataTransfer, opt_destinationPath, opt_effect) { | |
| 208 var sourcePaths = (dataTransfer.getData('fs/sources') || '').split('\n'); | |
| 209 var destinationPath = opt_destinationPath || | |
| 210 this.currentDirectoryContentPath; | |
| 211 // effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers | |
| 212 // work fine. | |
| 213 var effectAllowed = dataTransfer.effectAllowed != 'uninitialized' ? | |
| 214 dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed'); | |
| 215 var toMove = effectAllowed == 'move' || | |
| 216 (effectAllowed == 'copyMove' && opt_effect == 'move'); | |
| 217 | |
| 218 // Start the pasting operation. | |
| 219 this.fileOperationManager_.paste(sourcePaths, destinationPath, toMove); | |
| 220 return toMove ? 'move' : 'copy'; | |
| 221 }, | |
| 222 | |
| 223 /** | |
| 224 * Preloads an image thumbnail for the specified file entry. | |
| 225 * | |
| 226 * @this {FileTransferController} | |
| 227 * @param {Entry} entry Entry to preload a thumbnail for. | |
| 228 */ | |
| 229 preloadThumbnailImage_: function(entry) { | |
| 230 var imageUrl = entry.toURL(); | |
| 231 var metadataTypes = 'thumbnail|filesystem'; | |
| 232 var thumbnailContainer = this.document_.createElement('div'); | |
| 233 this.preloadedThumbnailImageNode_ = thumbnailContainer; | |
| 234 this.preloadedThumbnailImageNode_.className = 'img-container'; | |
| 235 this.metadataCache_.get( | |
| 236 imageUrl, | |
| 237 metadataTypes, | |
| 238 function(metadata) { | |
| 239 new ThumbnailLoader(imageUrl, | |
| 240 ThumbnailLoader.LoaderType.IMAGE, | |
| 241 metadata). | |
| 242 load(thumbnailContainer, | |
| 243 ThumbnailLoader.FillMode.FILL); | |
| 244 }.bind(this)); | |
| 245 }, | |
| 246 | |
| 247 /** | |
| 248 * Renders a drag-and-drop thumbnail. | |
| 249 * | |
| 250 * @this {FileTransferController} | |
| 251 * @return {HTMLElement} Element containing the thumbnail. | |
| 252 */ | |
| 253 renderThumbnail_: function() { | |
| 254 var length = this.selectedEntries_.length; | |
| 255 | |
| 256 var container = this.document_.querySelector('#drag-container'); | |
| 257 var contents = this.document_.createElement('div'); | |
| 258 contents.className = 'drag-contents'; | |
| 259 container.appendChild(contents); | |
| 260 | |
| 261 var thumbnailImage; | |
| 262 if (this.preloadedThumbnailImageNode_) | |
| 263 thumbnailImage = this.preloadedThumbnailImageNode_.querySelector('img'); | |
| 264 | |
| 265 // Option 1. Multiple selection, render only a label. | |
| 266 if (length > 1) { | |
| 267 var label = this.document_.createElement('div'); | |
| 268 label.className = 'label'; | |
| 269 label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length); | |
| 270 contents.appendChild(label); | |
| 271 return container; | |
| 272 } | |
| 273 | |
| 274 // Option 2. Thumbnail image available, then render it without | |
| 275 // a label. | |
| 276 if (thumbnailImage) { | |
| 277 thumbnailImage.classList.add('drag-thumbnail'); | |
| 278 contents.classList.add('for-image'); | |
| 279 contents.appendChild(this.preloadedThumbnailImageNode_); | |
| 280 return container; | |
| 281 } | |
| 282 | |
| 283 // Option 3. Thumbnail not available. Render an icon and a label. | |
| 284 var entry = this.selectedEntries_[0]; | |
| 285 var icon = this.document_.createElement('div'); | |
| 286 icon.className = 'detail-icon'; | |
| 287 icon.setAttribute('file-type-icon', FileType.getIcon(entry)); | |
| 288 contents.appendChild(icon); | |
| 289 var label = this.document_.createElement('div'); | |
| 290 label.className = 'label'; | |
| 291 label.textContent = entry.name; | |
| 292 contents.appendChild(label); | |
| 293 return container; | |
| 294 }, | |
| 295 | |
| 296 /** | |
| 297 * @this {FileTransferController} | |
| 298 * @param {cr.ui.List} list Drop target list | |
| 299 * @param {Event} event A dragstart event of DOM. | |
| 300 */ | |
| 301 onDragStart_: function(list, event) { | |
| 302 // If a user is touching, Files.app does not receive drag operations. | |
| 303 if (this.touching_) { | |
| 304 event.preventDefault(); | |
| 305 return; | |
| 306 } | |
| 307 | |
| 308 // Check if a drag selection should be initiated or not. | |
| 309 if (list.shouldStartDragSelection(event)) { | |
| 310 this.dragSelector_.startDragSelection(list, event); | |
| 311 return; | |
| 312 } | |
| 313 | |
| 314 // Nothing selected. | |
| 315 if (!this.selectedEntries_.length) { | |
| 316 event.preventDefault(); | |
| 317 return; | |
| 318 } | |
| 319 | |
| 320 var dt = event.dataTransfer; | |
| 321 | |
| 322 if (this.canCopyOrDrag_(dt)) { | |
| 323 if (this.canCutOrDrag_(dt)) | |
| 324 this.cutOrCopy_(dt, 'copyMove'); | |
| 325 else | |
| 326 this.cutOrCopy_(dt, 'copy'); | |
| 327 } else { | |
| 328 event.preventDefault(); | |
| 329 return; | |
| 330 } | |
| 331 | |
| 332 var dragThumbnail = this.renderThumbnail_(); | |
| 333 dt.setDragImage(dragThumbnail, 1000, 1000); | |
| 334 | |
| 335 window[DRAG_AND_DROP_GLOBAL_DATA] = { | |
| 336 sourceRoot: this.directoryModel_.getCurrentRootPath() | |
| 337 }; | |
| 338 }, | |
| 339 | |
| 340 /** | |
| 341 * @this {FileTransferController} | |
| 342 * @param {cr.ui.List} list Drop target list. | |
| 343 * @param {Event} event A dragend event of DOM. | |
| 344 */ | |
| 345 onDragEnd_: function(list, event) { | |
| 346 var container = this.document_.querySelector('#drag-container'); | |
| 347 container.textContent = ''; | |
| 348 this.clearDropTarget_(); | |
| 349 delete window[DRAG_AND_DROP_GLOBAL_DATA]; | |
| 350 }, | |
| 351 | |
| 352 /** | |
| 353 * @this {FileTransferController} | |
| 354 * @param {boolean} onlyIntoDirectories True if the drag is only into | |
| 355 * directories. | |
| 356 * @param {cr.ui.List} list Drop target list. | |
| 357 * @param {Event} event A dragover event of DOM. | |
| 358 */ | |
| 359 onDragOver_: function(onlyIntoDirectories, list, event) { | |
| 360 event.preventDefault(); | |
| 361 var path = this.destinationPath_ || | |
| 362 (!onlyIntoDirectories && this.currentDirectoryContentPath); | |
| 363 event.dataTransfer.dropEffect = this.selectDropEffect_(event, path); | |
| 364 event.preventDefault(); | |
| 365 }, | |
| 366 | |
| 367 /** | |
| 368 * @this {FileTransferController} | |
| 369 * @param {cr.ui.List} list Drop target list. | |
| 370 * @param {Event} event A dragenter event of DOM. | |
| 371 */ | |
| 372 onDragEnterFileList_: function(list, event) { | |
| 373 event.preventDefault(); // Required to prevent the cursor flicker. | |
| 374 this.lastEnteredTarget_ = event.target; | |
| 375 var item = list.getListItemAncestor(event.target); | |
| 376 item = item && list.isItem(item) ? item : null; | |
| 377 if (item == this.dropTarget_) | |
| 378 return; | |
| 379 | |
| 380 var entry = item && list.dataModel.item(item.listIndex); | |
| 381 if (entry) { | |
| 382 this.setDropTarget_(item, entry.isDirectory, event.dataTransfer, | |
| 383 entry.fullPath); | |
| 384 } else { | |
| 385 this.clearDropTarget_(); | |
| 386 } | |
| 387 }, | |
| 388 | |
| 389 /** | |
| 390 * @this {FileTransferController} | |
| 391 * @param {DirectoryTree} tree Drop target tree. | |
| 392 * @param {Event} event A dragenter event of DOM. | |
| 393 */ | |
| 394 onDragEnterTree_: function(tree, event) { | |
| 395 event.preventDefault(); // Required to prevent the cursor flicker. | |
| 396 this.lastEnteredTarget_ = event.target; | |
| 397 var item = event.target; | |
| 398 while (item && !(item instanceof DirectoryItem)) { | |
| 399 item = item.parentNode; | |
| 400 } | |
| 401 | |
| 402 if (item == this.dropTarget_) | |
| 403 return; | |
| 404 | |
| 405 var entry = item && item.entry; | |
| 406 if (entry) { | |
| 407 this.setDropTarget_(item, entry.isDirectory, event.dataTransfer, | |
| 408 entry.fullPath); | |
| 409 } else { | |
| 410 this.clearDropTarget_(); | |
| 411 } | |
| 412 }, | |
| 413 | |
| 414 /** | |
| 415 * @this {FileTransferController} | |
| 416 * @param {NavigationList} list Drop target list. | |
| 417 * @param {Event} event A dragenter event of DOM. | |
| 418 */ | |
| 419 onDragEnterVolumesList_: function(list, event) { | |
| 420 event.preventDefault(); // Required to prevent the cursor flicker. | |
| 421 this.lastEnteredTarget_ = event.target; | |
| 422 var item = list.getListItemAncestor(event.target); | |
| 423 item = item && list.isItem(item) ? item : null; | |
| 424 if (item == this.dropTarget_) | |
| 425 return; | |
| 426 | |
| 427 var path = item && list.dataModel.item(item.listIndex).path; | |
| 428 if (path) | |
| 429 this.setDropTarget_(item, true /* directory */, event.dataTransfer, path); | |
| 430 else | |
| 431 this.clearDropTarget_(); | |
| 432 }, | |
| 433 | |
| 434 /** | |
| 435 * @this {FileTransferController} | |
| 436 * @param {cr.ui.List} list Drop target list. | |
| 437 * @param {Event} event A dragleave event of DOM. | |
| 438 */ | |
| 439 onDragLeave_: function(list, event) { | |
| 440 // If mouse moves from one element to another the 'dragenter' | |
| 441 // event for the new element comes before the 'dragleave' event for | |
| 442 // the old one. In this case event.target != this.lastEnteredTarget_ | |
| 443 // and handler of the 'dragenter' event has already caried of | |
| 444 // drop target. So event.target == this.lastEnteredTarget_ | |
| 445 // could only be if mouse goes out of listened element. | |
| 446 if (event.target == this.lastEnteredTarget_) { | |
| 447 this.clearDropTarget_(); | |
| 448 this.lastEnteredTarget_ = null; | |
| 449 } | |
| 450 }, | |
| 451 | |
| 452 /** | |
| 453 * @this {FileTransferController} | |
| 454 * @param {boolean} onlyIntoDirectories True if the drag is only into | |
| 455 * directories. | |
| 456 * @param {Event} event A dragleave event of DOM. | |
| 457 */ | |
| 458 onDrop_: function(onlyIntoDirectories, event) { | |
| 459 if (onlyIntoDirectories && !this.dropTarget_) | |
| 460 return; | |
| 461 var destinationPath = this.destinationPath_ || | |
| 462 this.currentDirectoryContentPath; | |
| 463 if (!this.canPasteOrDrop_(event.dataTransfer, destinationPath)) | |
| 464 return; | |
| 465 event.preventDefault(); | |
| 466 this.paste(event.dataTransfer, destinationPath, | |
| 467 this.selectDropEffect_(event, destinationPath)); | |
| 468 this.clearDropTarget_(); | |
| 469 }, | |
| 470 | |
| 471 /** | |
| 472 * Sets the drop target. | |
| 473 * @this {FileTransferController} | |
| 474 * @param {Element} domElement Target of the drop. | |
| 475 * @param {boolean} isDirectory If the target is a directory. | |
| 476 * @param {DataTransfer} dataTransfer Data transfer object. | |
| 477 * @param {string} destinationPath Destination path. | |
| 478 */ | |
| 479 setDropTarget_: function(domElement, isDirectory, dataTransfer, | |
| 480 destinationPath) { | |
| 481 if (this.dropTarget_ == domElement) | |
| 482 return; | |
| 483 | |
| 484 // Remove the old drop target. | |
| 485 this.clearDropTarget_(); | |
| 486 | |
| 487 // Set the new drop target. | |
| 488 this.dropTarget_ = domElement; | |
| 489 | |
| 490 if (!domElement || | |
| 491 !isDirectory || | |
| 492 !this.canPasteOrDrop_(dataTransfer, destinationPath)) { | |
| 493 return; | |
| 494 } | |
| 495 | |
| 496 // Add accept class if the domElement can accept the drag. | |
| 497 domElement.classList.add('accepts'); | |
| 498 this.destinationPath_ = destinationPath; | |
| 499 | |
| 500 // Start timer changing the directory. | |
| 501 this.navigateTimer_ = setTimeout(function() { | |
| 502 if (domElement instanceof DirectoryItem) | |
| 503 // Do custom action. | |
| 504 (/** @type {DirectoryItem} */ domElement).doDropTargetAction(); | |
| 505 this.directoryModel_.changeDirectory(destinationPath); | |
| 506 }.bind(this), 2000); | |
| 507 }, | |
| 508 | |
| 509 /** | |
| 510 * Handles touch start. | |
| 511 */ | |
| 512 onTouchStart_: function() { | |
| 513 this.touching_ = true; | |
| 514 }, | |
| 515 | |
| 516 /** | |
| 517 * Handles touch end. | |
| 518 */ | |
| 519 onTouchEnd_: function(event) { | |
| 520 if (event.touches.length === 0) | |
| 521 this.touching_ = false; | |
| 522 }, | |
| 523 | |
| 524 /** | |
| 525 * Clears the drop target. | |
| 526 * @this {FileTransferController} | |
| 527 */ | |
| 528 clearDropTarget_: function() { | |
| 529 if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts')) | |
| 530 this.dropTarget_.classList.remove('accepts'); | |
| 531 this.dropTarget_ = null; | |
| 532 this.destinationPath_ = null; | |
| 533 if (this.navigateTimer_ !== undefined) { | |
| 534 clearTimeout(this.navigateTimer_); | |
| 535 this.navigateTimer_ = undefined; | |
| 536 } | |
| 537 }, | |
| 538 | |
| 539 /** | |
| 540 * @this {FileTransferController} | |
| 541 * @return {boolean} Returns false if {@code <input type="text">} element is | |
| 542 * currently active. Otherwise, returns true. | |
| 543 */ | |
| 544 isDocumentWideEvent_: function() { | |
| 545 return this.document_.activeElement.nodeName.toLowerCase() != 'input' || | |
| 546 this.document_.activeElement.type.toLowerCase() != 'text'; | |
| 547 }, | |
| 548 | |
| 549 /** | |
| 550 * @this {FileTransferController} | |
| 551 */ | |
| 552 onCopy_: function(event) { | |
| 553 if (!this.isDocumentWideEvent_() || | |
| 554 !this.canCopyOrDrag_()) { | |
| 555 return; | |
| 556 } | |
| 557 event.preventDefault(); | |
| 558 this.cutOrCopy_(event.clipboardData, 'copy'); | |
| 559 this.notify_('selection-copied'); | |
| 560 }, | |
| 561 | |
| 562 /** | |
| 563 * @this {FileTransferController} | |
| 564 */ | |
| 565 onBeforeCopy_: function(event) { | |
| 566 if (!this.isDocumentWideEvent_()) | |
| 567 return; | |
| 568 | |
| 569 // queryCommandEnabled returns true if event.defaultPrevented is true. | |
| 570 if (this.canCopyOrDrag_()) | |
| 571 event.preventDefault(); | |
| 572 }, | |
| 573 | |
| 574 /** | |
| 575 * @this {FileTransferController} | |
| 576 * @return {boolean} Returns true if some files are selected and all the file | |
| 577 * on drive is available to be copied. Otherwise, returns false. | |
| 578 */ | |
| 579 canCopyOrDrag_: function() { | |
| 580 if (this.isOnDrive && | |
| 581 this.directoryModel_.isDriveOffline() && | |
| 582 !this.allDriveFilesAvailable) | |
| 583 return false; | |
| 584 return this.selectedEntries_.length > 0; | |
| 585 }, | |
| 586 | |
| 587 /** | |
| 588 * @this {FileTransferController} | |
| 589 */ | |
| 590 onCut_: function(event) { | |
| 591 if (!this.isDocumentWideEvent_() || | |
| 592 !this.canCutOrDrag_()) { | |
| 593 return; | |
| 594 } | |
| 595 event.preventDefault(); | |
| 596 this.cutOrCopy_(event.clipboardData, 'move'); | |
| 597 this.notify_('selection-cut'); | |
| 598 }, | |
| 599 | |
| 600 /** | |
| 601 * @this {FileTransferController} | |
| 602 */ | |
| 603 onBeforeCut_: function(event) { | |
| 604 if (!this.isDocumentWideEvent_()) | |
| 605 return; | |
| 606 // queryCommandEnabled returns true if event.defaultPrevented is true. | |
| 607 if (this.canCutOrDrag_()) | |
| 608 event.preventDefault(); | |
| 609 }, | |
| 610 | |
| 611 /** | |
| 612 * @this {FileTransferController} | |
| 613 * @return {boolean} Returns true if some files are selected and all the file | |
| 614 * on drive is available to be cut. Otherwise, returns false. | |
| 615 */ | |
| 616 canCutOrDrag_: function() { | |
| 617 return !this.readonly && this.canCopyOrDrag_(); | |
| 618 }, | |
| 619 | |
| 620 /** | |
| 621 * @this {FileTransferController} | |
| 622 */ | |
| 623 onPaste_: function(event) { | |
| 624 // Need to update here since 'beforepaste' doesn't fire. | |
| 625 if (!this.isDocumentWideEvent_() || | |
| 626 !this.canPasteOrDrop_(event.clipboardData, | |
| 627 this.currentDirectoryContentPath)) { | |
| 628 return; | |
| 629 } | |
| 630 event.preventDefault(); | |
| 631 var effect = this.paste(event.clipboardData); | |
| 632 | |
| 633 // On cut, we clear the clipboard after the file is pasted/moved so we don't | |
| 634 // try to move/delete the original file again. | |
| 635 if (effect == 'move') { | |
| 636 this.simulateCommand_('cut', function(event) { | |
| 637 event.preventDefault(); | |
| 638 event.clipboardData.setData('fs/clear', ''); | |
| 639 }); | |
| 640 } | |
| 641 }, | |
| 642 | |
| 643 /** | |
| 644 * @this {FileTransferController} | |
| 645 */ | |
| 646 onBeforePaste_: function(event) { | |
| 647 if (!this.isDocumentWideEvent_()) | |
| 648 return; | |
| 649 // queryCommandEnabled returns true if event.defaultPrevented is true. | |
| 650 if (this.canPasteOrDrop_(event.clipboardData, | |
| 651 this.currentDirectoryContentPath)) { | |
| 652 event.preventDefault(); | |
| 653 } | |
| 654 }, | |
| 655 | |
| 656 /** | |
| 657 * @this {FileTransferController} | |
| 658 * @param {DataTransfer} dataTransfer Data transfer object. | |
| 659 * @param {string?} destinationPath Destination path. | |
| 660 * @return {boolean} Returns true if items stored in {@code dataTransfer} can | |
| 661 * be pasted to {@code destinationPath}. Otherwise, returns false. | |
| 662 */ | |
| 663 canPasteOrDrop_: function(dataTransfer, destinationPath) { | |
| 664 if (!destinationPath) { | |
| 665 return false; | |
| 666 } | |
| 667 if (this.directoryModel_.isPathReadOnly(destinationPath)) { | |
| 668 return false; | |
| 669 } | |
| 670 if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') == -1) { | |
| 671 return false; // Unsupported type of content. | |
| 672 } | |
| 673 if (dataTransfer.getData('fs/tag') == '') { | |
| 674 // Data protected. Other checks are not possible but it makes sense to | |
| 675 // let the user try. | |
| 676 return true; | |
| 677 } | |
| 678 | |
| 679 var directories = dataTransfer.getData('fs/directories').split('\n'). | |
| 680 filter(function(d) { return d != ''; }); | |
| 681 | |
| 682 for (var i = 0; i < directories.length; i++) { | |
| 683 if (destinationPath.substr(0, directories[i].length) == directories[i]) | |
| 684 return false; // recursive paste. | |
| 685 } | |
| 686 | |
| 687 return true; | |
| 688 }, | |
| 689 | |
| 690 /** | |
| 691 * Execute paste command. | |
| 692 * | |
| 693 * @this {FileTransferController} | |
| 694 * @return {boolean} Returns true, the paste is success. Otherwise, returns | |
| 695 * false. | |
| 696 */ | |
| 697 queryPasteCommandEnabled: function() { | |
| 698 if (!this.isDocumentWideEvent_()) { | |
| 699 return false; | |
| 700 } | |
| 701 | |
| 702 // HACK(serya): return this.document_.queryCommandEnabled('paste') | |
| 703 // should be used. | |
| 704 var result; | |
| 705 this.simulateCommand_('paste', function(event) { | |
| 706 result = this.canPasteOrDrop_(event.clipboardData, | |
| 707 this.currentDirectoryContentPath); | |
| 708 }.bind(this)); | |
| 709 return result; | |
| 710 }, | |
| 711 | |
| 712 /** | |
| 713 * Allows to simulate commands to get access to clipboard. | |
| 714 * | |
| 715 * @this {FileTransferController} | |
| 716 * @param {string} command 'copy', 'cut' or 'paste'. | |
| 717 * @param {function} handler Event handler. | |
| 718 */ | |
| 719 simulateCommand_: function(command, handler) { | |
| 720 var iframe = this.document_.querySelector('#command-dispatcher'); | |
| 721 var doc = iframe.contentDocument; | |
| 722 doc.addEventListener(command, handler); | |
| 723 doc.execCommand(command); | |
| 724 doc.removeEventListener(command, handler); | |
| 725 }, | |
| 726 | |
| 727 /** | |
| 728 * @this {FileTransferController} | |
| 729 */ | |
| 730 onSelectionChanged_: function(event) { | |
| 731 var entries = this.selectedEntries_; | |
| 732 var files = this.selectedFileObjects_ = []; | |
| 733 this.preloadedThumbnailImageNode_ = null; | |
| 734 | |
| 735 var fileEntries = []; | |
| 736 for (var i = 0; i < entries.length; i++) { | |
| 737 if (entries[i].isFile) | |
| 738 fileEntries.push(entries[i]); | |
| 739 } | |
| 740 | |
| 741 if (entries.length == 1) { | |
| 742 // For single selection, the dragged element is created in advance, | |
| 743 // otherwise an image may not be loaded at the time the 'dragstart' event | |
| 744 // comes. | |
| 745 this.preloadThumbnailImage_(entries[0]); | |
| 746 } | |
| 747 | |
| 748 // File object must be prepeared in advance for clipboard operations | |
| 749 // (copy, paste and drag). DataTransfer object closes for write after | |
| 750 // returning control from that handlers so they may not have | |
| 751 // asynchronous operations. | |
| 752 var prepareFileObjects = function() { | |
| 753 for (var i = 0; i < fileEntries.length; i++) { | |
| 754 fileEntries[i].file(function(file) { files.push(file); }); | |
| 755 } | |
| 756 }; | |
| 757 | |
| 758 if (this.isOnDrive) { | |
| 759 this.allDriveFilesAvailable = false; | |
| 760 var urls = entries.map(function(e) { return e.toURL() }); | |
| 761 this.metadataCache_.get( | |
| 762 urls, 'drive', function(props) { | |
| 763 // We consider directories not available offline for the purposes of | |
| 764 // file transfer since we cannot afford to recursive traversal. | |
| 765 this.allDriveFilesAvailable = | |
| 766 entries.filter(function(e) {return e.isDirectory}).length == 0 && | |
| 767 props.filter(function(p) {return !p.availableOffline}).length == 0; | |
| 768 // |Copy| is the only menu item affected by allDriveFilesAvailable. | |
| 769 // It could be open right now, update its UI. | |
| 770 this.copyCommand_.disabled = !this.canCopyOrDrag_(); | |
| 771 | |
| 772 if (this.allDriveFilesAvailable) | |
| 773 prepareFileObjects(); | |
| 774 }.bind(this)); | |
| 775 } else { | |
| 776 prepareFileObjects(); | |
| 777 } | |
| 778 }, | |
| 779 | |
| 780 /** | |
| 781 * Path of directory that is displaying now. | |
| 782 * If search result is displaying now, this is null. | |
| 783 * @this {FileTransferController} | |
| 784 * @return {string} Path of directry that is displaying now. | |
| 785 */ | |
| 786 get currentDirectoryContentPath() { | |
| 787 return this.directoryModel_.isSearching() ? | |
| 788 null : this.directoryModel_.getCurrentDirPath(); | |
| 789 }, | |
| 790 | |
| 791 /** | |
| 792 * @this {FileTransferController} | |
| 793 * @return {boolean} True if the current directory is read only. | |
| 794 */ | |
| 795 get readonly() { | |
| 796 return this.directoryModel_.isReadOnly(); | |
| 797 }, | |
| 798 | |
| 799 /** | |
| 800 * @this {FileTransferController} | |
| 801 * @return {boolean} True if the current directory is on Drive. | |
| 802 */ | |
| 803 get isOnDrive() { | |
| 804 return PathUtil.isDriveBasedPath(this.directoryModel_.getCurrentRootPath()); | |
| 805 }, | |
| 806 | |
| 807 /** | |
| 808 * @this {FileTransferController} | |
| 809 */ | |
| 810 notify_: function(eventName) { | |
| 811 var self = this; | |
| 812 // Set timeout to avoid recursive events. | |
| 813 setTimeout(function() { | |
| 814 cr.dispatchSimpleEvent(self, eventName); | |
| 815 }, 0); | |
| 816 }, | |
| 817 | |
| 818 /** | |
| 819 * @this {FileTransferController} | |
| 820 * @return {Array.<Entry>} Array of the selected entries. | |
| 821 */ | |
| 822 get selectedEntries_() { | |
| 823 var list = this.directoryModel_.getFileList(); | |
| 824 var selectedIndexes = this.directoryModel_.getFileListSelection(). | |
| 825 selectedIndexes; | |
| 826 var entries = selectedIndexes.map(function(index) { | |
| 827 return list.item(index); | |
| 828 }); | |
| 829 | |
| 830 // TODO(serya): Diagnostics for http://crbug/129642 | |
| 831 if (entries.indexOf(undefined) != -1) { | |
| 832 var index = entries.indexOf(undefined); | |
| 833 entries = entries.filter(function(e) { return !!e; }); | |
| 834 console.error('Invalid selection found: list items: ', list.length, | |
| 835 'wrong indexe value: ', selectedIndexes[index], | |
| 836 'Stack trace: ', new Error().stack); | |
| 837 } | |
| 838 return entries; | |
| 839 }, | |
| 840 | |
| 841 /** | |
| 842 * @this {FileTransferController} | |
| 843 * @return {string} Returns the appropriate drop query type ('none', 'move' | |
| 844 * or copy') to the current modifiers status and the destination. | |
| 845 */ | |
| 846 selectDropEffect_: function(event, destinationPath) { | |
| 847 if (!destinationPath || | |
| 848 this.directoryModel_.isPathReadOnly(destinationPath)) | |
| 849 return 'none'; | |
| 850 if (event.dataTransfer.effectAllowed == 'copyMove' && | |
| 851 this.getSourceRoot_(event.dataTransfer) == | |
| 852 PathUtil.getRootPath(destinationPath) && | |
| 853 !event.ctrlKey) { | |
| 854 return 'move'; | |
| 855 } | |
| 856 if (event.dataTransfer.effectAllowed == 'copyMove' && | |
| 857 event.shiftKey) { | |
| 858 return 'move'; | |
| 859 } | |
| 860 return 'copy'; | |
| 861 }, | |
| 862 }; | |
| OLD | NEW |