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 |