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 |