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 * FileManager constructor. | |
9 * | |
10 * FileManager objects encapsulate the functionality of the file selector | |
11 * dialogs, as well as the full screen file manager application (though the | |
12 * latter is not yet implemented). | |
13 * | |
14 * @constructor | |
15 */ | |
16 function FileManager() { | |
17 this.initializeQueue_ = new AsyncUtil.Group(); | |
18 | |
19 /** | |
20 * Current list type. | |
21 * @type {ListType} | |
22 * @private | |
23 */ | |
24 this.listType_ = null; | |
25 | |
26 /** | |
27 * Whether to suppress the focus moving or not. | |
28 * This is used to filter out focusing by mouse. | |
29 * @type {boolean} | |
30 * @private | |
31 */ | |
32 this.suppressFocus_ = false; | |
33 | |
34 /** | |
35 * SelectionHandler. | |
36 * @type {SelectionHandler} | |
37 * @private | |
38 */ | |
39 this.selectionHandler_ = null; | |
40 } | |
41 | |
42 /** | |
43 * Maximum delay in milliseconds for updating thumbnails in the bottom panel | |
44 * to mitigate flickering. If images load faster then the delay they replace | |
45 * old images smoothly. On the other hand we don't want to keep old images | |
46 * too long. | |
47 * | |
48 * @type {number} | |
49 * @const | |
50 */ | |
51 FileManager.THUMBNAIL_SHOW_DELAY = 100; | |
52 | |
53 FileManager.prototype = { | |
54 __proto__: cr.EventTarget.prototype, | |
55 get directoryModel() { | |
56 return this.directoryModel_; | |
57 }, | |
58 get navigationList() { | |
59 return this.navigationList_; | |
60 }, | |
61 get document() { | |
62 return this.document_; | |
63 }, | |
64 get fileTransferController() { | |
65 return this.fileTransferController_; | |
66 }, | |
67 get backgroundPage() { | |
68 return this.backgroundPage_; | |
69 } | |
70 }; | |
71 | |
72 /** | |
73 * Unload the file manager. | |
74 * Used by background.js (when running in the packaged mode). | |
75 */ | |
76 function unload() { | |
77 fileManager.onBeforeUnload_(); | |
78 fileManager.onUnload_(); | |
79 } | |
80 | |
81 /** | |
82 * List of dialog types. | |
83 * | |
84 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except | |
85 * FULL_PAGE which is specific to this code. | |
86 * | |
87 * @enum {string} | |
88 */ | |
89 var DialogType = { | |
90 SELECT_FOLDER: 'folder', | |
91 SELECT_UPLOAD_FOLDER: 'upload-folder', | |
92 SELECT_SAVEAS_FILE: 'saveas-file', | |
93 SELECT_OPEN_FILE: 'open-file', | |
94 SELECT_OPEN_MULTI_FILE: 'open-multi-file', | |
95 FULL_PAGE: 'full-page' | |
96 }; | |
97 | |
98 /** | |
99 * @param {string} type Dialog type. | |
100 * @return {boolean} Whether the type is modal. | |
101 */ | |
102 DialogType.isModal = function(type) { | |
103 return type == DialogType.SELECT_FOLDER || | |
104 type == DialogType.SELECT_UPLOAD_FOLDER || | |
105 type == DialogType.SELECT_SAVEAS_FILE || | |
106 type == DialogType.SELECT_OPEN_FILE || | |
107 type == DialogType.SELECT_OPEN_MULTI_FILE; | |
108 }; | |
109 | |
110 /** | |
111 * @param {string} type Dialog type. | |
112 * @return {boolean} Whether the type is open dialog. | |
113 */ | |
114 DialogType.isOpenDialog = function(type) { | |
115 return type == DialogType.SELECT_OPEN_FILE || | |
116 type == DialogType.SELECT_OPEN_MULTI_FILE; | |
117 }; | |
118 | |
119 /** | |
120 * Bottom margin of the list and tree for transparent preview panel. | |
121 * @const | |
122 */ | |
123 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52; | |
124 | |
125 // Anonymous "namespace". | |
126 (function() { | |
127 | |
128 // Private variables and helper functions. | |
129 | |
130 /** | |
131 * Number of milliseconds in a day. | |
132 */ | |
133 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; | |
134 | |
135 /** | |
136 * Some UI elements react on a single click and standard double click handling | |
137 * leads to confusing results. We ignore a second click if it comes soon | |
138 * after the first. | |
139 */ | |
140 var DOUBLE_CLICK_TIMEOUT = 200; | |
141 | |
142 /** | |
143 * Update the element to display the information about remaining space for | |
144 * the storage. | |
145 * @param {!Element} spaceInnerBar Block element for a percentage bar | |
146 * representing the remaining space. | |
147 * @param {!Element} spaceInfoLabel Inline element to contain the message. | |
148 * @param {!Element} spaceOuterBar Block element around the percentage bar. | |
149 */ | |
150 var updateSpaceInfo = function( | |
151 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) { | |
152 spaceInnerBar.removeAttribute('pending'); | |
153 if (sizeStatsResult) { | |
154 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize); | |
155 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr); | |
156 | |
157 var usedSpace = | |
158 sizeStatsResult.totalSize - sizeStatsResult.remainingSize; | |
159 spaceInnerBar.style.width = | |
160 (100 * usedSpace / sizeStatsResult.totalSize) + '%'; | |
161 | |
162 spaceOuterBar.hidden = false; | |
163 } else { | |
164 spaceOuterBar.hidden = true; | |
165 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO'); | |
166 } | |
167 }; | |
168 | |
169 // Public statics. | |
170 | |
171 FileManager.ListType = { | |
172 DETAIL: 'detail', | |
173 THUMBNAIL: 'thumb' | |
174 }; | |
175 | |
176 FileManager.prototype.initPreferences_ = function(callback) { | |
177 var group = new AsyncUtil.Group(); | |
178 | |
179 // DRIVE preferences should be initialized before creating DirectoryModel | |
180 // to rebuild the roots list. | |
181 group.add(this.getPreferences_.bind(this)); | |
182 | |
183 // Get startup preferences. | |
184 this.viewOptions_ = {}; | |
185 group.add(function(done) { | |
186 util.platform.getPreference(this.startupPrefName_, function(value) { | |
187 // Load the global default options. | |
188 try { | |
189 this.viewOptions_ = JSON.parse(value); | |
190 } catch (ignore) {} | |
191 // Override with window-specific options. | |
192 if (window.appState && window.appState.viewOptions) { | |
193 for (var key in window.appState.viewOptions) { | |
194 if (window.appState.viewOptions.hasOwnProperty(key)) | |
195 this.viewOptions_[key] = window.appState.viewOptions[key]; | |
196 } | |
197 } | |
198 done(); | |
199 }.bind(this)); | |
200 }.bind(this)); | |
201 | |
202 // Get the command line option. | |
203 group.add(function(done) { | |
204 chrome.commandLinePrivate.hasSwitch( | |
205 'file-manager-show-checkboxes', function(flag) { | |
206 this.showCheckboxes_ = flag; | |
207 done(); | |
208 }.bind(this)); | |
209 }.bind(this)); | |
210 | |
211 // TODO(yoshiki): Remove the flag when the feature is launched. | |
212 this.enableExperimentalWebstoreIntegration_ = false; | |
213 group.add(function(done) { | |
214 chrome.commandLinePrivate.hasSwitch( | |
215 'file-manager-enable-webstore-integration', function(flag) { | |
216 this.enableExperimentalWebstoreIntegration_ = flag; | |
217 done(); | |
218 }.bind(this)); | |
219 }.bind(this)); | |
220 | |
221 group.run(callback); | |
222 }; | |
223 | |
224 /** | |
225 * One time initialization for the file system and related things. | |
226 * | |
227 * @param {function()} callback Completion callback. | |
228 * @private | |
229 */ | |
230 FileManager.prototype.initFileSystemUI_ = function(callback) { | |
231 this.table_.startBatchUpdates(); | |
232 this.grid_.startBatchUpdates(); | |
233 | |
234 this.initFileList_(); | |
235 this.setupCurrentDirectory_(); | |
236 | |
237 // PyAuto tests monitor this state by polling this variable | |
238 this.__defineGetter__('workerInitialized_', function() { | |
239 return this.metadataCache_.isInitialized(); | |
240 }.bind(this)); | |
241 | |
242 this.initDateTimeFormatters_(); | |
243 | |
244 var self = this; | |
245 | |
246 // Get the 'allowRedeemOffers' preference before launching | |
247 // FileListBannerController. | |
248 this.getPreferences_(function(pref) { | |
249 /** @type {boolean} */ | |
250 var showOffers = pref['allowRedeemOffers']; | |
251 self.bannersController_ = new FileListBannerController( | |
252 self.directoryModel_, self.volumeManager_, self.document_, | |
253 showOffers); | |
254 self.bannersController_.addEventListener('relayout', | |
255 self.onResize_.bind(self)); | |
256 }); | |
257 | |
258 var dm = this.directoryModel_; | |
259 dm.addEventListener('directory-changed', | |
260 this.onDirectoryChanged_.bind(this)); | |
261 dm.addEventListener('begin-update-files', function() { | |
262 self.currentList_.startBatchUpdates(); | |
263 }); | |
264 dm.addEventListener('end-update-files', function() { | |
265 self.restoreItemBeingRenamed_(); | |
266 self.currentList_.endBatchUpdates(); | |
267 }); | |
268 dm.addEventListener('scan-started', this.onScanStarted_.bind(this)); | |
269 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this)); | |
270 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this)); | |
271 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this)); | |
272 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this)); | |
273 dm.addEventListener('rescan-completed', | |
274 this.onRescanCompleted_.bind(this)); | |
275 | |
276 var sm = this.directoryModel_.getFileListSelection(); | |
277 sm.addEventListener('change', function() { | |
278 if (sm.selectedIndexes.length != 1) | |
279 return; | |
280 var view = (this.listType_ == FileManager.ListType.DETAIL) ? | |
281 this.table_.list : this.grid_; | |
282 var selectedItem = view.getListItemByIndex(sm.selectedIndex); | |
283 if (!selectedItem) | |
284 return; | |
285 this.ensureItemNotBehindPreviewPanel_(selectedItem, view); | |
286 }.bind(this)); | |
287 | |
288 this.directoryTree_.addEventListener('change', function() { | |
289 var selectedSubTree = this.directoryTree_.selectedItem; | |
290 if (!selectedSubTree) | |
291 return; | |
292 var selectedItem = selectedSubTree.rowElement; | |
293 this.ensureItemNotBehindPreviewPanel_(selectedItem, this.directoryTree_); | |
294 }.bind(this)); | |
295 | |
296 var stateChangeHandler = | |
297 this.onPreferencesChanged_.bind(this); | |
298 chrome.fileBrowserPrivate.onPreferencesChanged.addListener( | |
299 stateChangeHandler); | |
300 stateChangeHandler(); | |
301 | |
302 var driveConnectionChangedHandler = | |
303 this.onDriveConnectionChanged_.bind(this); | |
304 this.volumeManager_.addEventListener('drive-connection-changed', | |
305 driveConnectionChangedHandler); | |
306 driveConnectionChangedHandler(); | |
307 | |
308 // Set the initial focus. | |
309 this.refocus(); | |
310 // Set it as a fallback when there is no focus. | |
311 this.document_.addEventListener('focusout', function(e) { | |
312 setTimeout(function() { | |
313 // When there is no focus, the active element is the <body>. | |
314 if (this.document_.activeElement == this.document_.body) | |
315 this.refocus(); | |
316 }.bind(this), 0); | |
317 }.bind(this)); | |
318 | |
319 this.initDataTransferOperations_(); | |
320 | |
321 this.initContextMenus_(); | |
322 this.initCommands_(); | |
323 | |
324 this.updateFileTypeFilter_(); | |
325 | |
326 this.selectionHandler_.onFileSelectionChanged(); | |
327 | |
328 this.table_.endBatchUpdates(); | |
329 this.grid_.endBatchUpdates(); | |
330 | |
331 callback(); | |
332 }; | |
333 | |
334 /** | |
335 * If |item| in |parentView| is behind the preview panel, scrolls up the | |
336 * parent view and make the item visible. This should be called when: | |
337 * - the selected item is changed. | |
338 * - the visibility of the the preview panel is changed. | |
339 * | |
340 * @param {HTMLElement} item Item to be visible in the parent. | |
341 * @param {HTMLElement} parentView View contains |selectedItem|. | |
342 * @private | |
343 */ | |
344 FileManager.prototype.ensureItemNotBehindPreviewPanel_ = | |
345 function(item, parentView) { | |
346 var itemRect = item.getBoundingClientRect(); | |
347 if (!itemRect) | |
348 return; | |
349 | |
350 var listRect = parentView.getBoundingClientRect(); | |
351 if (!listRect) | |
352 return; | |
353 | |
354 var previewPanel = this.dialogDom_.querySelector('.preview-panel'); | |
355 var previewPanelRect = previewPanel.getBoundingClientRect(); | |
356 var panelHeight = previewPanelRect ? previewPanelRect.height : 0; | |
357 | |
358 var itemBottom = itemRect.bottom; | |
359 var listBottom = listRect.bottom - panelHeight; | |
360 | |
361 if (itemBottom > listBottom) { | |
362 var scrollOffset = itemBottom - listBottom; | |
363 parentView.scrollTop += scrollOffset; | |
364 } | |
365 }; | |
366 | |
367 /** | |
368 * @private | |
369 */ | |
370 FileManager.prototype.initDateTimeFormatters_ = function() { | |
371 var use12hourClock = !this.preferences_['use24hourClock']; | |
372 this.table_.setDateTimeFormat(use12hourClock); | |
373 }; | |
374 | |
375 /** | |
376 * @private | |
377 */ | |
378 FileManager.prototype.initDataTransferOperations_ = function() { | |
379 this.fileOperationManager_ = FileOperationManagerWrapper.getInstance( | |
380 this.backgroundPage_); | |
381 | |
382 this.butterBar_ = new ButterBar( | |
383 this.dialogDom_, this.fileOperationManager_); | |
384 | |
385 // CopyManager and ButterBar are required for 'Delete' operation in | |
386 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed. | |
387 if (this.dialogType != DialogType.FULL_PAGE) return; | |
388 | |
389 // TODO(hidehiko): Extract FileOperationManager related code from | |
390 // FileManager to simplify it. | |
391 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this); | |
392 this.fileOperationManager_.addEventListener( | |
393 'copy-progress', this.onCopyProgressBound_); | |
394 | |
395 this.onEntryChangedBound_ = this.onEntryChanged_.bind(this); | |
396 this.fileOperationManager_.addEventListener( | |
397 'entry-changed', this.onEntryChangedBound_); | |
398 | |
399 var controller = this.fileTransferController_ = | |
400 new FileTransferController(this.document_, | |
401 this.fileOperationManager_, | |
402 this.metadataCache_, | |
403 this.directoryModel_); | |
404 controller.attachDragSource(this.table_.list); | |
405 controller.attachFileListDropTarget(this.table_.list); | |
406 controller.attachDragSource(this.grid_); | |
407 controller.attachFileListDropTarget(this.grid_); | |
408 controller.attachTreeDropTarget(this.directoryTree_); | |
409 controller.attachNavigationListDropTarget(this.navigationList_, true); | |
410 controller.attachCopyPasteHandlers(); | |
411 controller.addEventListener('selection-copied', | |
412 this.blinkSelection.bind(this)); | |
413 controller.addEventListener('selection-cut', | |
414 this.blinkSelection.bind(this)); | |
415 }; | |
416 | |
417 /** | |
418 * One-time initialization of context menus. | |
419 * @private | |
420 */ | |
421 FileManager.prototype.initContextMenus_ = function() { | |
422 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu'); | |
423 cr.ui.Menu.decorate(this.fileContextMenu_); | |
424 | |
425 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_); | |
426 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'), | |
427 this.fileContextMenu_); | |
428 cr.ui.contextMenuHandler.setContextMenu( | |
429 this.document_.querySelector('.drive-welcome.page'), | |
430 this.fileContextMenu_); | |
431 | |
432 this.rootsContextMenu_ = | |
433 this.dialogDom_.querySelector('#roots-context-menu'); | |
434 cr.ui.Menu.decorate(this.rootsContextMenu_); | |
435 this.navigationList_.setContextMenu(this.rootsContextMenu_); | |
436 | |
437 this.directoryTreeContextMenu_ = | |
438 this.dialogDom_.querySelector('#directory-tree-context-menu'); | |
439 cr.ui.Menu.decorate(this.directoryTreeContextMenu_); | |
440 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_; | |
441 | |
442 this.textContextMenu_ = | |
443 this.dialogDom_.querySelector('#text-context-menu'); | |
444 cr.ui.Menu.decorate(this.textContextMenu_); | |
445 | |
446 this.gearButton_ = this.dialogDom_.querySelector('#gear-button'); | |
447 this.gearButton_.addEventListener('menushow', | |
448 this.refreshRemainingSpace_.bind(this, | |
449 false /* Without loading caption. */)); | |
450 this.dialogDom_.querySelector('#gear-menu').menuItemSelector = | |
451 'menuitem, hr'; | |
452 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton); | |
453 | |
454 if (this.dialogType == DialogType.FULL_PAGE) { | |
455 // This is to prevent the buttons from stealing focus on mouse down. | |
456 var preventFocus = function(event) { | |
457 event.preventDefault(); | |
458 }; | |
459 | |
460 var maximizeButton = this.dialogDom_.querySelector('#maximize-button'); | |
461 maximizeButton.addEventListener('click', this.onMaximize.bind(this)); | |
462 maximizeButton.addEventListener('mousedown', preventFocus); | |
463 | |
464 var closeButton = this.dialogDom_.querySelector('#close-button'); | |
465 closeButton.addEventListener('click', this.onClose.bind(this)); | |
466 closeButton.addEventListener('mousedown', preventFocus); | |
467 } | |
468 | |
469 this.syncButton.checkable = true; | |
470 this.hostedButton.checkable = true; | |
471 this.detailViewButton_.checkable = true; | |
472 this.thumbnailViewButton_.checkable = true; | |
473 | |
474 if (util.platform.runningInBrowser()) { | |
475 // Suppresses the default context menu. | |
476 this.dialogDom_.addEventListener('contextmenu', function(e) { | |
477 e.preventDefault(); | |
478 e.stopPropagation(); | |
479 }); | |
480 } | |
481 }; | |
482 | |
483 FileManager.prototype.onMaximize = function() { | |
484 var appWindow = chrome.app.window.current(); | |
485 if (appWindow.isMaximized()) | |
486 appWindow.restore(); | |
487 else | |
488 appWindow.maximize(); | |
489 }; | |
490 | |
491 FileManager.prototype.onClose = function() { | |
492 window.close(); | |
493 }; | |
494 | |
495 /** | |
496 * One-time initialization of commands. | |
497 * @private | |
498 */ | |
499 FileManager.prototype.initCommands_ = function() { | |
500 this.commandHandler = new CommandHandler(this); | |
501 | |
502 // TODO(hirono): Move the following block to the UI part. | |
503 var commandButtons = this.dialogDom_.querySelectorAll('button[command]'); | |
504 for (var j = 0; j < commandButtons.length; j++) | |
505 CommandButton.decorate(commandButtons[j]); | |
506 | |
507 var inputs = this.dialogDom_.querySelectorAll( | |
508 'input[type=text], input[type=search], textarea'); | |
509 for (var i = 0; i < inputs.length; i++) { | |
510 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_); | |
511 this.registerInputCommands_(inputs[i]); | |
512 } | |
513 | |
514 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_, | |
515 this.textContextMenu_); | |
516 this.registerInputCommands_(this.renameInput_); | |
517 this.document_.addEventListener('command', | |
518 this.setNoHover_.bind(this, true)); | |
519 }; | |
520 | |
521 /** | |
522 * Registers cut, copy, paste and delete commands on input element. | |
523 * | |
524 * @param {Node} node Text input element to register on. | |
525 * @private | |
526 */ | |
527 FileManager.prototype.registerInputCommands_ = function(node) { | |
528 CommandUtil.forceDefaultHandler(node, 'cut'); | |
529 CommandUtil.forceDefaultHandler(node, 'copy'); | |
530 CommandUtil.forceDefaultHandler(node, 'paste'); | |
531 CommandUtil.forceDefaultHandler(node, 'delete'); | |
532 node.addEventListener('keydown', function(e) { | |
533 if (util.getKeyModifiers(e) + e.keyCode == '191') { | |
534 // If this key event is propagated, this is handled search command, | |
535 // which calls 'preventDefault' method. | |
536 e.stopPropagation(); | |
537 } | |
538 }); | |
539 }; | |
540 | |
541 /** | |
542 * Entry point of the initialization. | |
543 * This method is called from main.js. | |
544 */ | |
545 FileManager.prototype.initializeCore = function() { | |
546 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral'); | |
547 this.initializeQueue_.add(this.initStrings_.bind(this), [], 'initStrings'); | |
548 this.initializeQueue_.add(this.initBackgroundPage_.bind(this), | |
549 [], 'initBackgroundPage'); | |
550 this.initializeQueue_.add(this.initPreferences_.bind(this), | |
551 ['initGeneral'], 'initPreferences'); | |
552 this.initializeQueue_.add(this.initVolumeManager_.bind(this), | |
553 ['initGeneral', 'initBackgroundPage'], | |
554 'initVolumeManager'); | |
555 | |
556 this.initializeQueue_.run(); | |
557 }; | |
558 | |
559 FileManager.prototype.initializeUI = function(dialogDom, callback) { | |
560 this.dialogDom_ = dialogDom; | |
561 this.document_ = this.dialogDom_.ownerDocument; | |
562 | |
563 this.initializeQueue_.add( | |
564 this.initEssentialUI_.bind(this), | |
565 ['initGeneral', 'initStrings'], | |
566 'initEssentialUI'); | |
567 this.initializeQueue_.add(this.initAdditionalUI_.bind(this), | |
568 ['initEssentialUI', 'initBackgroundPage'], 'initAdditionalUI'); | |
569 this.initializeQueue_.add( | |
570 this.initFileSystemUI_.bind(this), | |
571 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI'); | |
572 | |
573 // Run again just in case if all pending closures have completed and the | |
574 // queue has stopped and monitor the completion. | |
575 this.initializeQueue_.run(callback); | |
576 }; | |
577 | |
578 /** | |
579 * Initializes general purpose basic things, which are used by other | |
580 * initializing methods. | |
581 * | |
582 * @param {function()} callback Completion callback. | |
583 * @private | |
584 */ | |
585 FileManager.prototype.initGeneral_ = function(callback) { | |
586 // Initialize the application state. | |
587 if (window.appState) { | |
588 this.params_ = window.appState.params || {}; | |
589 this.defaultPath = window.appState.defaultPath; | |
590 } else { | |
591 this.params_ = location.search ? | |
592 JSON.parse(decodeURIComponent(location.search.substr(1))) : | |
593 {}; | |
594 this.defaultPath = this.params_.defaultPath; | |
595 } | |
596 | |
597 // Initialize the member variables that depend this.params_. | |
598 this.dialogType = this.params_.type || DialogType.FULL_PAGE; | |
599 this.startupPrefName_ = 'file-manager-' + this.dialogType; | |
600 this.fileTypes_ = this.params_.typeList || []; | |
601 | |
602 callback(); | |
603 }; | |
604 | |
605 /** | |
606 * One time initialization of strings (mostly i18n). | |
607 * | |
608 * @param {function()} callback Completion callback. | |
609 * @private | |
610 */ | |
611 FileManager.prototype.initStrings_ = function(callback) { | |
612 // Fetch the strings via the private api if running in the browser window. | |
613 // Otherwise, read cached strings from the local storage. | |
614 if (util.platform.runningInBrowser()) { | |
615 chrome.fileBrowserPrivate.getStrings(function(strings) { | |
616 loadTimeData.data = strings; | |
617 callback(); | |
618 }); | |
619 } else { | |
620 chrome.storage.local.get('strings', function(items) { | |
621 loadTimeData.data = items['strings']; | |
622 callback(); | |
623 }); | |
624 } | |
625 }; | |
626 | |
627 /** | |
628 * Initialize the background page. | |
629 * @param {function()} callback Completion callback. | |
630 * @private | |
631 */ | |
632 FileManager.prototype.initBackgroundPage_ = function(callback) { | |
633 chrome.runtime.getBackgroundPage(function(backgroundPage) { | |
634 this.backgroundPage_ = backgroundPage; | |
635 callback(); | |
636 }.bind(this)); | |
637 }; | |
638 | |
639 /** | |
640 * Initializes the VolumeManager instance. | |
641 * @param {function()} callback Completion callback. | |
642 * @private | |
643 */ | |
644 FileManager.prototype.initVolumeManager_ = function(callback) { | |
645 // Auto resolving to local path does not work for folders (e.g., dialog for | |
646 // loading unpacked extensions). | |
647 var noLocalPathResolution = | |
648 this.params_.type == DialogType.SELECT_FOLDER || | |
649 this.params_.type == DialogType.SELECT_UPLOAD_FOLDER; | |
650 | |
651 // If this condition is false, VolumeManagerWrapper hides all drive | |
652 // related event and data, even if Drive is enabled on preference. | |
653 // In other words, even if Drive is disabled on preference but Files.app | |
654 // should show Drive when it is re-enabled, then the value should be set to | |
655 // true. | |
656 // Note that the Drive enabling preference change is listened by | |
657 // DriveIntegrationService, so here we don't need to take care about it. | |
658 var driveEnabled = | |
659 !noLocalPathResolution || !this.params_.shouldReturnLocalPath; | |
660 this.volumeManager_ = new VolumeManagerWrapper( | |
661 driveEnabled, this.backgroundPage_); | |
662 callback(); | |
663 }; | |
664 | |
665 /** | |
666 * One time initialization of the Files.app's essential UI elements. These | |
667 * elements will be shown to the user. Only visible elements should be | |
668 * initialized here. Any heavy operation should be avoided. Files.app's | |
669 * window is shown at the end of this routine. | |
670 * | |
671 * @param {function()} callback Completion callback. | |
672 * @private | |
673 */ | |
674 FileManager.prototype.initEssentialUI_ = function(callback) { | |
675 // Optional list of file types. | |
676 metrics.recordEnum('Create', this.dialogType, | |
677 [DialogType.SELECT_FOLDER, | |
678 DialogType.SELECT_UPLOAD_FOLDER, | |
679 DialogType.SELECT_SAVEAS_FILE, | |
680 DialogType.SELECT_OPEN_FILE, | |
681 DialogType.SELECT_OPEN_MULTI_FILE, | |
682 DialogType.FULL_PAGE]); | |
683 | |
684 // Create the metadata cache. | |
685 this.metadataCache_ = MetadataCache.createFull(); | |
686 | |
687 // Create the root view of FileManager. | |
688 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType); | |
689 this.fileTypeSelector_ = this.ui_.fileTypeSelector; | |
690 this.okButton_ = this.ui_.okButton; | |
691 this.cancelButton_ = this.ui_.cancelButton; | |
692 | |
693 // Show the window as soon as the UI pre-initialization is done. | |
694 if (this.dialogType == DialogType.FULL_PAGE && | |
695 !util.platform.runningInBrowser()) { | |
696 chrome.app.window.current().show(); | |
697 setTimeout(callback, 100); // Wait until the animation is finished. | |
698 } else { | |
699 callback(); | |
700 } | |
701 }; | |
702 | |
703 /** | |
704 * One-time initialization of dialogs. | |
705 * @private | |
706 */ | |
707 FileManager.prototype.initDialogs_ = function() { | |
708 // Initialize the dialog. | |
709 this.ui_.initDialogs(); | |
710 FileManagerDialogBase.setFileManager(this); | |
711 | |
712 // Obtains the dialog instances from FileManagerUI. | |
713 // TODO(hirono): Remove the properties from the FileManager class. | |
714 this.error = this.ui_.errorDialog; | |
715 this.alert = this.ui_.alertDialog; | |
716 this.confirm = this.ui_.confirmDialog; | |
717 this.prompt = this.ui_.promptDialog; | |
718 this.shareDialog_ = this.ui_.shareDialog; | |
719 this.defaultTaskPicker = this.ui_.defaultTaskPicker; | |
720 this.suggestAppsDialog = this.ui_.suggestAppsDialog; | |
721 }; | |
722 | |
723 /** | |
724 * One-time initialization of various DOM nodes. Loads the additional DOM | |
725 * elements visible to the user. Initialize here elements, which are expensive | |
726 * or hidden in the beginning. | |
727 * | |
728 * @param {function()} callback Completion callback. | |
729 * @private | |
730 */ | |
731 FileManager.prototype.initAdditionalUI_ = function(callback) { | |
732 this.initDialogs_(); | |
733 this.ui_.initAdditionalUI(); | |
734 | |
735 this.dialogDom_.addEventListener('drop', function(e) { | |
736 // Prevent opening an URL by dropping it onto the page. | |
737 e.preventDefault(); | |
738 }); | |
739 | |
740 this.dialogDom_.addEventListener('click', | |
741 this.onExternalLinkClick_.bind(this)); | |
742 // Cache nodes we'll be manipulating. | |
743 var dom = this.dialogDom_; | |
744 | |
745 this.filenameInput_ = dom.querySelector('#filename-input-box input'); | |
746 this.taskItems_ = dom.querySelector('#tasks'); | |
747 | |
748 this.table_ = dom.querySelector('.detail-table'); | |
749 this.grid_ = dom.querySelector('.thumbnail-grid'); | |
750 this.spinner_ = dom.querySelector('#spinner-with-text'); | |
751 this.showSpinner_(true); | |
752 | |
753 // Check the option to hide the selecting checkboxes. | |
754 this.table_.showCheckboxes = this.showCheckboxes_; | |
755 | |
756 var fullPage = this.dialogType == DialogType.FULL_PAGE; | |
757 FileTable.decorate(this.table_, this.metadataCache_, fullPage); | |
758 FileGrid.decorate(this.grid_, this.metadataCache_); | |
759 | |
760 this.previewPanel_ = new PreviewPanel( | |
761 dom.querySelector('.preview-panel'), | |
762 DialogType.isOpenDialog(this.dialogType) ? | |
763 PreviewPanel.VisibilityType.ALWAYS_VISIBLE : | |
764 PreviewPanel.VisibilityType.AUTO, | |
765 this.getCurrentDirectory(), | |
766 this.metadataCache_); | |
767 this.previewPanel_.addEventListener( | |
768 PreviewPanel.Event.VISIBILITY_CHANGE, | |
769 this.onPreviewPanelVisibilityChange_.bind(this)); | |
770 this.previewPanel_.initialize(); | |
771 | |
772 this.previewPanel_.breadcrumbs.addEventListener( | |
773 'pathclick', this.onBreadcrumbClick_.bind(this)); | |
774 | |
775 // Initialize progress center panel. | |
776 this.progressCenterPanel_ = new ProgressCenterPanel( | |
777 dom.querySelector('#progress-center'), | |
778 this.backgroundPage_.progressCenter.requestCancel.bind( | |
779 this.backgroundPage_.progressCenter)); | |
780 var initialItems = this.backgroundPage_.progressCenter.applicationItems; | |
781 for (var i = 0; i < initialItems.length; i++) { | |
782 this.progressCenterPanel_.updateItem( | |
783 initialItems[i], | |
784 this.backgroundPage_.progressCenter.getSummarizedItem()); | |
785 } | |
786 this.backgroundPage_.progressCenter.addEventListener( | |
787 ProgressCenterEvent.ITEM_UPDATED, | |
788 function(event) { | |
789 this.progressCenterPanel_.updateItem( | |
790 event.item, | |
791 this.backgroundPage_.progressCenter.getSummarizedItem()); | |
792 }.bind(this)); | |
793 this.backgroundPage_.progressCenter.addEventListener( | |
794 ProgressCenterEvent.RESET, | |
795 function(event) { | |
796 this.progressCenterPanel_.reset(); | |
797 }.bind(this)); | |
798 | |
799 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this)); | |
800 | |
801 // This capturing event is only used to distinguish focusing using | |
802 // keyboard from focusing using mouse. | |
803 this.document_.addEventListener('mousedown', function() { | |
804 this.suppressFocus_ = true; | |
805 }.bind(this), true); | |
806 | |
807 this.renameInput_ = this.document_.createElement('input'); | |
808 this.renameInput_.className = 'rename'; | |
809 | |
810 this.renameInput_.addEventListener( | |
811 'keydown', this.onRenameInputKeyDown_.bind(this)); | |
812 this.renameInput_.addEventListener( | |
813 'blur', this.onRenameInputBlur_.bind(this)); | |
814 | |
815 this.filenameInput_.addEventListener( | |
816 'keydown', this.onFilenameInputKeyDown_.bind(this)); | |
817 this.filenameInput_.addEventListener( | |
818 'focus', this.onFilenameInputFocus_.bind(this)); | |
819 | |
820 this.listContainer_ = this.dialogDom_.querySelector('#list-container'); | |
821 this.listContainer_.addEventListener( | |
822 'keydown', this.onListKeyDown_.bind(this)); | |
823 this.listContainer_.addEventListener( | |
824 'keypress', this.onListKeyPress_.bind(this)); | |
825 this.listContainer_.addEventListener( | |
826 'mousemove', this.onListMouseMove_.bind(this)); | |
827 | |
828 this.okButton_.addEventListener('click', this.onOk_.bind(this)); | |
829 this.onCancelBound_ = this.onCancel_.bind(this); | |
830 this.cancelButton_.addEventListener('click', this.onCancelBound_); | |
831 | |
832 this.decorateSplitter( | |
833 this.dialogDom_.querySelector('#navigation-list-splitter')); | |
834 this.decorateSplitter( | |
835 this.dialogDom_.querySelector('#middlebar-splitter')); | |
836 | |
837 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container'); | |
838 | |
839 this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings'); | |
840 this.syncButton.addEventListener('click', this.onDrivePrefClick_.bind( | |
841 this, 'cellularDisabled', false /* not inverted */)); | |
842 | |
843 this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings'); | |
844 this.hostedButton.addEventListener('click', this.onDrivePrefClick_.bind( | |
845 this, 'hostedFilesDisabled', true /* inverted */)); | |
846 | |
847 this.detailViewButton_ = | |
848 this.dialogDom_.querySelector('#detail-view'); | |
849 this.detailViewButton_.addEventListener('click', | |
850 this.onDetailViewButtonClick_.bind(this)); | |
851 | |
852 this.thumbnailViewButton_ = | |
853 this.dialogDom_.querySelector('#thumbnail-view'); | |
854 this.thumbnailViewButton_.addEventListener('click', | |
855 this.onThumbnailViewButtonClick_.bind(this)); | |
856 | |
857 cr.ui.ComboButton.decorate(this.taskItems_); | |
858 this.taskItems_.showMenu = function(shouldSetFocus) { | |
859 // Prevent the empty menu from opening. | |
860 if (!this.menu.length) | |
861 return; | |
862 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus); | |
863 }; | |
864 this.taskItems_.addEventListener('select', | |
865 this.onTaskItemClicked_.bind(this)); | |
866 | |
867 this.dialogDom_.ownerDocument.defaultView.addEventListener( | |
868 'resize', this.onResize_.bind(this)); | |
869 | |
870 this.filePopup_ = null; | |
871 | |
872 this.searchBoxWrapper_ = this.ui_.searchBox.element; | |
873 this.searchBox_ = this.ui_.searchBox.inputElement; | |
874 this.searchBox_.addEventListener( | |
875 'input', this.onSearchBoxUpdate_.bind(this)); | |
876 this.ui_.searchBox.clearButton.addEventListener( | |
877 'click', this.onSearchClearButtonClick_.bind(this)); | |
878 | |
879 this.lastSearchQuery_ = ''; | |
880 | |
881 this.autocompleteList_ = this.ui_.searchBox.autocompleteList; | |
882 this.autocompleteList_.requestSuggestions = | |
883 this.requestAutocompleteSuggestions_.bind(this); | |
884 | |
885 // Instead, open the suggested item when Enter key is pressed or | |
886 // mouse-clicked. | |
887 this.autocompleteList_.handleEnterKeydown = function(event) { | |
888 this.openAutocompleteSuggestion_(); | |
889 this.lastAutocompleteQuery_ = ''; | |
890 this.autocompleteList_.suggestions = []; | |
891 }.bind(this); | |
892 this.autocompleteList_.addEventListener('mousedown', function(event) { | |
893 this.openAutocompleteSuggestion_(); | |
894 this.lastAutocompleteQuery_ = ''; | |
895 this.autocompleteList_.suggestions = []; | |
896 }.bind(this)); | |
897 | |
898 this.defaultActionMenuItem_ = | |
899 this.dialogDom_.querySelector('#default-action'); | |
900 | |
901 this.openWithCommand_ = | |
902 this.dialogDom_.querySelector('#open-with'); | |
903 | |
904 this.driveBuyMoreStorageCommand_ = | |
905 this.dialogDom_.querySelector('#drive-buy-more-space'); | |
906 | |
907 this.defaultActionMenuItem_.addEventListener('click', | |
908 this.dispatchSelectionAction_.bind(this)); | |
909 | |
910 this.initFileTypeFilter_(); | |
911 | |
912 util.addIsFocusedMethod(); | |
913 | |
914 // Populate the static localized strings. | |
915 i18nTemplate.process(this.document_, loadTimeData); | |
916 | |
917 // Arrange the file list. | |
918 this.table_.normalizeColumns(); | |
919 this.table_.redraw(); | |
920 | |
921 callback(); | |
922 }; | |
923 | |
924 /** | |
925 * @private | |
926 */ | |
927 FileManager.prototype.onBreadcrumbClick_ = function(event) { | |
928 this.directoryModel_.changeDirectory(event.path); | |
929 }; | |
930 | |
931 /** | |
932 * Constructs table and grid (heavy operation). | |
933 * @private | |
934 **/ | |
935 FileManager.prototype.initFileList_ = function() { | |
936 // Always sharing the data model between the detail/thumb views confuses | |
937 // them. Instead we maintain this bogus data model, and hook it up to the | |
938 // view that is not in use. | |
939 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]); | |
940 this.emptySelectionModel_ = new cr.ui.ListSelectionModel(); | |
941 | |
942 var singleSelection = | |
943 this.dialogType == DialogType.SELECT_OPEN_FILE || | |
944 this.dialogType == DialogType.SELECT_FOLDER || | |
945 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER || | |
946 this.dialogType == DialogType.SELECT_SAVEAS_FILE; | |
947 | |
948 var showSpecialSearchRoots = | |
949 this.dialogType == DialogType.SELECT_OPEN_FILE || | |
950 this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE || | |
951 this.dialogType == DialogType.FULL_PAGE; | |
952 | |
953 this.fileFilter_ = new FileFilter( | |
954 this.metadataCache_, | |
955 false /* Don't show dot files by default. */); | |
956 | |
957 this.fileWatcher_ = new FileWatcher(this.metadataCache_); | |
958 this.fileWatcher_.addEventListener( | |
959 'watcher-metadata-changed', | |
960 this.onWatcherMetadataChanged_.bind(this)); | |
961 | |
962 this.directoryModel_ = new DirectoryModel( | |
963 singleSelection, | |
964 this.fileFilter_, | |
965 this.fileWatcher_, | |
966 this.metadataCache_, | |
967 this.volumeManager_, | |
968 showSpecialSearchRoots); | |
969 | |
970 this.folderShortcutsModel_ = new FolderShortcutsDataModel(); | |
971 | |
972 this.selectionHandler_ = new FileSelectionHandler(this); | |
973 | |
974 var dataModel = this.directoryModel_.getFileList(); | |
975 | |
976 this.table_.setupCompareFunctions(dataModel); | |
977 | |
978 dataModel.addEventListener('permuted', | |
979 this.updateStartupPrefs_.bind(this)); | |
980 | |
981 this.directoryModel_.getFileListSelection().addEventListener('change', | |
982 this.selectionHandler_.onFileSelectionChanged.bind( | |
983 this.selectionHandler_)); | |
984 | |
985 this.initList_(this.grid_); | |
986 this.initList_(this.table_.list); | |
987 | |
988 var fileListFocusBound = this.onFileListFocus_.bind(this); | |
989 var fileListBlurBound = this.onFileListBlur_.bind(this); | |
990 | |
991 this.table_.list.addEventListener('focus', fileListFocusBound); | |
992 this.grid_.addEventListener('focus', fileListFocusBound); | |
993 | |
994 this.table_.list.addEventListener('blur', fileListBlurBound); | |
995 this.grid_.addEventListener('blur', fileListBlurBound); | |
996 | |
997 var dragStartBound = this.onDragStart_.bind(this); | |
998 this.table_.list.addEventListener('dragstart', dragStartBound); | |
999 this.grid_.addEventListener('dragstart', dragStartBound); | |
1000 | |
1001 var dragEndBound = this.onDragEnd_.bind(this); | |
1002 this.table_.list.addEventListener('dragend', dragEndBound); | |
1003 this.grid_.addEventListener('dragend', dragEndBound); | |
1004 // This event is published by DragSelector because drag end event is not | |
1005 // published at the end of drag selection. | |
1006 this.table_.list.addEventListener('dragselectionend', dragEndBound); | |
1007 this.grid_.addEventListener('dragselectionend', dragEndBound); | |
1008 | |
1009 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just | |
1010 // attach the directory model. | |
1011 this.initNavigationList_(); | |
1012 | |
1013 this.table_.addEventListener('column-resize-end', | |
1014 this.updateStartupPrefs_.bind(this)); | |
1015 | |
1016 // Restore preferences. | |
1017 this.directoryModel_.sortFileList( | |
1018 this.viewOptions_.sortField || 'modificationTime', | |
1019 this.viewOptions_.sortDirection || 'desc'); | |
1020 if (this.viewOptions_.columns) { | |
1021 var cm = this.table_.columnModel; | |
1022 for (var i = 0; i < cm.totalSize; i++) { | |
1023 if (this.viewOptions_.columns[i] > 0) | |
1024 cm.setWidth(i, this.viewOptions_.columns[i]); | |
1025 } | |
1026 } | |
1027 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL); | |
1028 | |
1029 this.textSearchState_ = {text: '', date: new Date()}; | |
1030 this.closeOnUnmount_ = (this.params_.action == 'auto-open'); | |
1031 | |
1032 if (this.closeOnUnmount_) { | |
1033 this.volumeManager_.addEventListener('externally-unmounted', | |
1034 this.onExternallyUnmounted_.bind(this)); | |
1035 } | |
1036 | |
1037 // Update metadata to change 'Today' and 'Yesterday' dates. | |
1038 var today = new Date(); | |
1039 today.setHours(0); | |
1040 today.setMinutes(0); | |
1041 today.setSeconds(0); | |
1042 today.setMilliseconds(0); | |
1043 setTimeout(this.dailyUpdateModificationTime_.bind(this), | |
1044 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000); | |
1045 }; | |
1046 | |
1047 /** | |
1048 * @private | |
1049 */ | |
1050 FileManager.prototype.initNavigationList_ = function() { | |
1051 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree'); | |
1052 DirectoryTree.decorate(this.directoryTree_, this.directoryModel_); | |
1053 | |
1054 this.navigationList_ = this.dialogDom_.querySelector('#navigation-list'); | |
1055 NavigationList.decorate(this.navigationList_, | |
1056 this.volumeManager_, | |
1057 this.directoryModel_); | |
1058 this.navigationList_.fileManager = this; | |
1059 this.navigationList_.dataModel = new NavigationListModel( | |
1060 this.volumeManager_, this.folderShortcutsModel_); | |
1061 }; | |
1062 | |
1063 /** | |
1064 * @private | |
1065 */ | |
1066 FileManager.prototype.updateMiddleBarVisibility_ = function() { | |
1067 var entry = this.directoryModel_.getCurrentDirEntry(); | |
1068 if (!entry) | |
1069 return; | |
1070 | |
1071 var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); | |
1072 var visible = | |
1073 DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath) && | |
1074 driveVolume && !driveVolume.error; | |
1075 this.dialogDom_. | |
1076 querySelector('.dialog-middlebar-contents').hidden = !visible; | |
1077 this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible; | |
1078 this.onResize_(); | |
1079 }; | |
1080 | |
1081 /** | |
1082 * @private | |
1083 */ | |
1084 FileManager.prototype.updateStartupPrefs_ = function() { | |
1085 var sortStatus = this.directoryModel_.getFileList().sortStatus; | |
1086 var prefs = { | |
1087 sortField: sortStatus.field, | |
1088 sortDirection: sortStatus.direction, | |
1089 columns: [], | |
1090 listType: this.listType_ | |
1091 }; | |
1092 var cm = this.table_.columnModel; | |
1093 for (var i = 0; i < cm.totalSize; i++) { | |
1094 prefs.columns.push(cm.getWidth(i)); | |
1095 } | |
1096 // Save the global default. | |
1097 util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs)); | |
1098 | |
1099 // Save the window-specific preference. | |
1100 if (window.appState) { | |
1101 window.appState.viewOptions = prefs; | |
1102 util.saveAppState(); | |
1103 } | |
1104 }; | |
1105 | |
1106 FileManager.prototype.refocus = function() { | |
1107 var targetElement; | |
1108 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) | |
1109 targetElement = this.filenameInput_; | |
1110 else | |
1111 targetElement = this.currentList_; | |
1112 | |
1113 // Hack: if the tabIndex is disabled, we can assume a modal dialog is | |
1114 // shown. Focus to a button on the dialog instead. | |
1115 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1) | |
1116 targetElement = document.querySelector('button:not([tabIndex="-1"])'); | |
1117 | |
1118 if (targetElement) | |
1119 targetElement.focus(); | |
1120 }; | |
1121 | |
1122 /** | |
1123 * File list focus handler. Used to select the top most element on the list | |
1124 * if nothing was selected. | |
1125 * | |
1126 * @private | |
1127 */ | |
1128 FileManager.prototype.onFileListFocus_ = function() { | |
1129 // Do not select default item if focused using mouse. | |
1130 if (this.suppressFocus_) | |
1131 return; | |
1132 | |
1133 var selection = this.getSelection(); | |
1134 if (!selection || selection.totalCount != 0) | |
1135 return; | |
1136 | |
1137 this.directoryModel_.selectIndex(0); | |
1138 }; | |
1139 | |
1140 /** | |
1141 * File list blur handler. | |
1142 * | |
1143 * @private | |
1144 */ | |
1145 FileManager.prototype.onFileListBlur_ = function() { | |
1146 this.suppressFocus_ = false; | |
1147 }; | |
1148 | |
1149 /** | |
1150 * Index of selected item in the typeList of the dialog params. | |
1151 * | |
1152 * @return {number} 1-based index of selected type or 0 if no type selected. | |
1153 * @private | |
1154 */ | |
1155 FileManager.prototype.getSelectedFilterIndex_ = function() { | |
1156 var index = Number(this.fileTypeSelector_.selectedIndex); | |
1157 if (index < 0) // Nothing selected. | |
1158 return 0; | |
1159 if (this.params_.includeAllFiles) // Already 1-based. | |
1160 return index; | |
1161 return index + 1; // Convert to 1-based; | |
1162 }; | |
1163 | |
1164 FileManager.prototype.setListType = function(type) { | |
1165 if (type && type == this.listType_) | |
1166 return; | |
1167 | |
1168 this.table_.list.startBatchUpdates(); | |
1169 this.grid_.startBatchUpdates(); | |
1170 | |
1171 // TODO(dzvorygin): style.display and dataModel setting order shouldn't | |
1172 // cause any UI bugs. Currently, the only right way is first to set display | |
1173 // style and only then set dataModel. | |
1174 | |
1175 if (type == FileManager.ListType.DETAIL) { | |
1176 this.table_.dataModel = this.directoryModel_.getFileList(); | |
1177 this.table_.selectionModel = this.directoryModel_.getFileListSelection(); | |
1178 this.table_.hidden = false; | |
1179 this.grid_.hidden = true; | |
1180 this.grid_.selectionModel = this.emptySelectionModel_; | |
1181 this.grid_.dataModel = this.emptyDataModel_; | |
1182 this.table_.hidden = false; | |
1183 /** @type {cr.ui.List} */ | |
1184 this.currentList_ = this.table_.list; | |
1185 this.detailViewButton_.setAttribute('checked', ''); | |
1186 this.thumbnailViewButton_.removeAttribute('checked'); | |
1187 this.detailViewButton_.setAttribute('disabled', ''); | |
1188 this.thumbnailViewButton_.removeAttribute('disabled'); | |
1189 } else if (type == FileManager.ListType.THUMBNAIL) { | |
1190 this.grid_.dataModel = this.directoryModel_.getFileList(); | |
1191 this.grid_.selectionModel = this.directoryModel_.getFileListSelection(); | |
1192 this.grid_.hidden = false; | |
1193 this.table_.hidden = true; | |
1194 this.table_.selectionModel = this.emptySelectionModel_; | |
1195 this.table_.dataModel = this.emptyDataModel_; | |
1196 this.grid_.hidden = false; | |
1197 /** @type {cr.ui.List} */ | |
1198 this.currentList_ = this.grid_; | |
1199 this.thumbnailViewButton_.setAttribute('checked', ''); | |
1200 this.detailViewButton_.removeAttribute('checked'); | |
1201 this.thumbnailViewButton_.setAttribute('disabled', ''); | |
1202 this.detailViewButton_.removeAttribute('disabled'); | |
1203 } else { | |
1204 throw new Error('Unknown list type: ' + type); | |
1205 } | |
1206 | |
1207 this.listType_ = type; | |
1208 this.updateStartupPrefs_(); | |
1209 this.onResize_(); | |
1210 | |
1211 this.table_.list.endBatchUpdates(); | |
1212 this.grid_.endBatchUpdates(); | |
1213 }; | |
1214 | |
1215 /** | |
1216 * Initialize the file list table or grid. | |
1217 * | |
1218 * @param {cr.ui.List} list The list. | |
1219 * @private | |
1220 */ | |
1221 FileManager.prototype.initList_ = function(list) { | |
1222 // Overriding the default role 'list' to 'listbox' for better accessibility | |
1223 // on ChromeOS. | |
1224 list.setAttribute('role', 'listbox'); | |
1225 list.addEventListener('click', this.onDetailClick_.bind(this)); | |
1226 list.id = 'file-list'; | |
1227 }; | |
1228 | |
1229 /** | |
1230 * @private | |
1231 */ | |
1232 FileManager.prototype.onCopyProgress_ = function(event) { | |
1233 if (event.reason == 'ERROR' && | |
1234 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR && | |
1235 event.error.data.toDrive && | |
1236 event.error.data.code == FileError.QUOTA_EXCEEDED_ERR) { | |
1237 this.alert.showHtml( | |
1238 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'), | |
1239 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE', | |
1240 decodeURIComponent( | |
1241 event.error.data.sourceFileUrl.split('/').pop()), | |
1242 urlConstants.GOOGLE_DRIVE_BUY_STORAGE)); | |
1243 } | |
1244 | |
1245 // TODO(benchan): Currently, there is no FileWatcher emulation for | |
1246 // drive::FileSystem, so we need to manually trigger the directory rescan | |
1247 // after paste operations complete. Remove this once we emulate file | |
1248 // watching functionalities in drive::FileSystem. | |
1249 if (this.isOnDrive()) { | |
1250 if (event.reason == 'SUCCESS' || event.reason == 'ERROR' || | |
1251 event.reason == 'CANCELLED') { | |
1252 this.directoryModel_.rescanLater(); | |
1253 } | |
1254 } | |
1255 }; | |
1256 | |
1257 /** | |
1258 * Handler of file manager operations. Called when an entry has been | |
1259 * changed. | |
1260 * This updates directory model to reflect operation result immediately (not | |
1261 * waiting for directory update event). Also, preloads thumbnails for the | |
1262 * images of new entries. | |
1263 * See also FileOperationManager.EventRouter. | |
1264 * | |
1265 * @param {Event} event An event for the entry change. | |
1266 * @private | |
1267 */ | |
1268 FileManager.prototype.onEntryChanged_ = function(event) { | |
1269 var kind = event.kind; | |
1270 var entry = event.entry; | |
1271 this.directoryModel_.onEntryChanged(kind, entry); | |
1272 this.selectionHandler_.onFileSelectionChanged(); | |
1273 | |
1274 if (kind == util.EntryChangedKind.CREATE && FileType.isImage(entry)) { | |
1275 // Preload a thumbnail if the new copied entry an image. | |
1276 var metadata = entry.getMetadata(function(metadata) { | |
1277 var url = entry.toURL(); | |
1278 var thumbnailLoader_ = new ThumbnailLoader( | |
1279 url, | |
1280 ThumbnailLoader.LoaderType.CANVAS, | |
1281 metadata, | |
1282 undefined, // Media type. | |
1283 FileType.isOnDrive(url) ? | |
1284 ThumbnailLoader.UseEmbedded.USE_EMBEDDED : | |
1285 ThumbnailLoader.UseEmbedded.NO_EMBEDDED, | |
1286 10); // Very low priority. | |
1287 thumbnailLoader_.loadDetachedImage(function(success) {}); | |
1288 }); | |
1289 } | |
1290 }; | |
1291 | |
1292 /** | |
1293 * Fills the file type list or hides it. | |
1294 * @private | |
1295 */ | |
1296 FileManager.prototype.initFileTypeFilter_ = function() { | |
1297 if (this.params_.includeAllFiles) { | |
1298 var option = this.document_.createElement('option'); | |
1299 option.innerText = str('ALL_FILES_FILTER'); | |
1300 this.fileTypeSelector_.appendChild(option); | |
1301 option.value = 0; | |
1302 } | |
1303 | |
1304 for (var i = 0; i < this.fileTypes_.length; i++) { | |
1305 var fileType = this.fileTypes_[i]; | |
1306 var option = this.document_.createElement('option'); | |
1307 var description = fileType.description; | |
1308 if (!description) { | |
1309 // See if all the extensions in the group have the same description. | |
1310 for (var j = 0; j != fileType.extensions.length; j++) { | |
1311 var currentDescription = | |
1312 FileType.getTypeString('.' + fileType.extensions[j]); | |
1313 if (!description) // Set the first time. | |
1314 description = currentDescription; | |
1315 else if (description != currentDescription) { | |
1316 // No single description, fall through to the extension list. | |
1317 description = null; | |
1318 break; | |
1319 } | |
1320 } | |
1321 | |
1322 if (!description) | |
1323 // Convert ['jpg', 'png'] to '*.jpg, *.png'. | |
1324 description = fileType.extensions.map(function(s) { | |
1325 return '*.' + s; | |
1326 }).join(', '); | |
1327 } | |
1328 option.innerText = description; | |
1329 | |
1330 option.value = i + 1; | |
1331 | |
1332 if (fileType.selected) | |
1333 option.selected = true; | |
1334 | |
1335 this.fileTypeSelector_.appendChild(option); | |
1336 } | |
1337 | |
1338 var options = this.fileTypeSelector_.querySelectorAll('option'); | |
1339 if (options.length < 2) { | |
1340 // There is in fact no choice, hide the selector. | |
1341 this.fileTypeSelector_.hidden = true; | |
1342 return; | |
1343 } | |
1344 | |
1345 this.fileTypeSelector_.addEventListener('change', | |
1346 this.updateFileTypeFilter_.bind(this)); | |
1347 }; | |
1348 | |
1349 /** | |
1350 * Filters file according to the selected file type. | |
1351 * @private | |
1352 */ | |
1353 FileManager.prototype.updateFileTypeFilter_ = function() { | |
1354 this.fileFilter_.removeFilter('fileType'); | |
1355 var selectedIndex = this.getSelectedFilterIndex_(); | |
1356 if (selectedIndex > 0) { // Specific filter selected. | |
1357 var regexp = new RegExp('.*(' + | |
1358 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i'); | |
1359 var filter = function(entry) { | |
1360 return entry.isDirectory || regexp.test(entry.name); | |
1361 }; | |
1362 this.fileFilter_.addFilter('fileType', filter); | |
1363 } | |
1364 }; | |
1365 | |
1366 /** | |
1367 * Resize details and thumb views to fit the new window size. | |
1368 * @private | |
1369 */ | |
1370 FileManager.prototype.onResize_ = function() { | |
1371 if (this.listType_ == FileManager.ListType.THUMBNAIL) | |
1372 this.grid_.relayout(); | |
1373 else | |
1374 this.table_.relayout(); | |
1375 | |
1376 // May not be available during initialization. | |
1377 if (this.directoryTree_) | |
1378 this.directoryTree_.relayout(); | |
1379 | |
1380 // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before | |
1381 // file system is available. | |
1382 if (this.navigationList_) | |
1383 this.navigationList_.redraw(); | |
1384 | |
1385 this.ui_.searchBox.updateSizeRelatedStyle(); | |
1386 | |
1387 this.previewPanel_.breadcrumbs.truncate(); | |
1388 }; | |
1389 | |
1390 /** | |
1391 * Handles local metadata changes in the currect directory. | |
1392 * @param {Event} event Change event. | |
1393 * @private | |
1394 */ | |
1395 FileManager.prototype.onWatcherMetadataChanged_ = function(event) { | |
1396 this.updateMetadataInUI_(event.metadataType, event.urls, event.properties); | |
1397 }; | |
1398 | |
1399 /** | |
1400 * Resize details and thumb views to fit the new window size. | |
1401 * @private | |
1402 */ | |
1403 FileManager.prototype.onPreviewPanelVisibilityChange_ = function() { | |
1404 // This method may be called on initialization. Some object may not be | |
1405 // initialized. | |
1406 | |
1407 var panelHeight = this.previewPanel_.visible ? | |
1408 this.previewPanel_.height : 0; | |
1409 if (this.grid_) | |
1410 this.grid_.setBottomMarginForPanel(panelHeight); | |
1411 if (this.table_) | |
1412 this.table_.setBottomMarginForPanel(panelHeight); | |
1413 if (this.directoryTree_) | |
1414 this.directoryTree_.setBottomMarginForPanel(panelHeight); | |
1415 | |
1416 // Make sure that the selected item is not behind the preview panel. | |
1417 if (this.directoryModel_) { | |
1418 var sm = this.directoryModel_.getFileListSelection(); | |
1419 var view = (this.listType_ == FileManager.ListType.DETAIL) ? | |
1420 this.table_.list : this.grid_; | |
1421 var selectedItem = view.getListItemByIndex(sm.selectedIndex); | |
1422 if (!selectedItem) | |
1423 return; | |
1424 this.ensureItemNotBehindPreviewPanel_(selectedItem, view); | |
1425 } | |
1426 }; | |
1427 | |
1428 /** | |
1429 * Invoked when the drag is started on the list or the grid. | |
1430 * @private | |
1431 */ | |
1432 FileManager.prototype.onDragStart_ = function() { | |
1433 // On open file dialog, the preview panel is always shown. | |
1434 if (DialogType.isOpenDialog(this.dialogType)) | |
1435 return; | |
1436 this.previewPanel_.visibilityType = | |
1437 PreviewPanel.VisibilityType.ALWAYS_HIDDEN; | |
1438 }; | |
1439 | |
1440 /** | |
1441 * Invoked when the drag is ended on the list or the grid. | |
1442 * @private | |
1443 */ | |
1444 FileManager.prototype.onDragEnd_ = function() { | |
1445 // On open file dialog, the preview panel is always shown. | |
1446 if (DialogType.isOpenDialog(this.dialogType)) | |
1447 return; | |
1448 this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO; | |
1449 }; | |
1450 | |
1451 /** | |
1452 * Restores current directory and may be a selected item after page load (or | |
1453 * reload) or popping a state (after click on back/forward). If location.hash | |
1454 * is present it means that the user has navigated somewhere and that place | |
1455 * will be restored. defaultPath primarily is used with save/open dialogs. | |
1456 * Default path may also contain a file name. Freshly opened file manager | |
1457 * window has neither. | |
1458 * | |
1459 * @private | |
1460 */ | |
1461 FileManager.prototype.setupCurrentDirectory_ = function() { | |
1462 var path = location.hash ? // Location hash has the highest priority. | |
1463 decodeURIComponent(location.hash.substr(1)) : | |
1464 this.defaultPath; | |
1465 | |
1466 if (!path) { | |
1467 path = PathUtil.DEFAULT_DIRECTORY; | |
1468 } else if (path.indexOf('/') == -1) { | |
1469 // Path is a file name. | |
1470 path = PathUtil.DEFAULT_DIRECTORY + '/' + path; | |
1471 } | |
1472 | |
1473 var tracker = this.directoryModel_.createDirectoryChangeTracker(); | |
1474 tracker.start(); | |
1475 this.volumeManager_.ensureInitialized(function() { | |
1476 tracker.stop(); | |
1477 if (tracker.hasChanged) | |
1478 return; | |
1479 | |
1480 // If Drive is disabled but the path points to Drive's entry, | |
1481 // fallback to DEFAULT_DIRECTORY. | |
1482 if (PathUtil.isDriveBasedPath(path) && | |
1483 !this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) | |
1484 path = PathUtil.DEFAULT_DIRECTORY + '/' + PathUtil.basename(path); | |
1485 | |
1486 this.finishSetupCurrentDirectory_(path); | |
1487 }.bind(this)); | |
1488 }; | |
1489 | |
1490 /** | |
1491 * @param {string} path Path to setup. | |
1492 * @private | |
1493 */ | |
1494 FileManager.prototype.finishSetupCurrentDirectory_ = function(path) { | |
1495 this.directoryModel_.setupPath(path, function(baseName, leafName, exists) { | |
1496 if (this.dialogType == DialogType.FULL_PAGE) { | |
1497 // In the FULL_PAGE mode if the hash path points to a file we might have | |
1498 // to invoke a task after selecting it. | |
1499 // If the file path is in params_ we only want to select the file. | |
1500 if (this.params_.action == 'select') | |
1501 return; | |
1502 | |
1503 var task = null; | |
1504 if (!exists || leafName == '') { | |
1505 // Non-existent file or a directory. | |
1506 if (this.params_.gallery) { | |
1507 // Reloading while the Gallery is open with empty or multiple | |
1508 // selection. Open the Gallery when the directory is scanned. | |
1509 task = function() { | |
1510 new FileTasks(this, this.params_).openGallery([]); | |
1511 }.bind(this); | |
1512 } | |
1513 } else { | |
1514 // There are 3 ways we can get here: | |
1515 // 1. Invoked from file_manager_util::ViewFile. This can only | |
1516 // happen for 'gallery' and 'mount-archive' actions. | |
1517 // 2. Reloading a Gallery page. Must be an image or a video file. | |
1518 // 3. A user manually entered a URL pointing to a file. | |
1519 // We call the appropriate methods of FileTasks directly as we do | |
1520 // not need any of the preparations that |execute| method does. | |
1521 var mediaType = FileType.getMediaType(path); | |
1522 if (mediaType == 'image' || mediaType == 'video') { | |
1523 task = function() { | |
1524 new FileTasks(this, this.params_).openGallery( | |
1525 [util.makeFilesystemUrl(path)]); | |
1526 }.bind(this); | |
1527 } else if (mediaType == 'archive') { | |
1528 task = function() { | |
1529 new FileTasks(this, this.params_).mountArchives( | |
1530 [util.makeFilesystemUrl(path)]); | |
1531 }.bind(this); | |
1532 } | |
1533 } | |
1534 | |
1535 // If there is a task to be run, run it after the scan is completed. | |
1536 if (task) { | |
1537 var listener = function() { | |
1538 this.directoryModel_.removeEventListener( | |
1539 'scan-completed', listener); | |
1540 task(); | |
1541 }.bind(this); | |
1542 this.directoryModel_.addEventListener('scan-completed', listener); | |
1543 } | |
1544 return; | |
1545 } | |
1546 | |
1547 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) { | |
1548 this.filenameInput_.value = leafName; | |
1549 this.selectDefaultPathInFilenameInput_(); | |
1550 return; | |
1551 } | |
1552 }.bind(this)); | |
1553 }; | |
1554 | |
1555 /** | |
1556 * Unmounts device. | |
1557 * @param {string} path Path to a volume to unmount. | |
1558 */ | |
1559 FileManager.prototype.unmountVolume = function(path) { | |
1560 var onError = function(error) { | |
1561 this.alert.showHtml('', str('UNMOUNT_FAILED')); | |
1562 }; | |
1563 this.volumeManager_.unmount(path, function() {}, onError.bind(this)); | |
1564 }; | |
1565 | |
1566 /** | |
1567 * @private | |
1568 */ | |
1569 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() { | |
1570 var entries = this.directoryModel_.getFileList().slice(); | |
1571 var directoryEntry = this.directoryModel_.getCurrentDirEntry(); | |
1572 if (!directoryEntry) | |
1573 return; | |
1574 // We don't pass callback here. When new metadata arrives, we have an | |
1575 // observer registered to update the UI. | |
1576 | |
1577 // TODO(dgozman): refresh content metadata only when modificationTime | |
1578 // changed. | |
1579 var isFakeEntry = typeof directoryEntry.toURL !== 'function'; | |
1580 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries); | |
1581 this.metadataCache_.clearRecursively(directoryEntry, '*'); | |
1582 this.metadataCache_.get(getEntries, 'filesystem', null); | |
1583 | |
1584 if (this.isOnDrive()) | |
1585 this.metadataCache_.get(getEntries, 'drive', null); | |
1586 | |
1587 var visibleItems = this.currentList_.items; | |
1588 var visibleEntries = []; | |
1589 for (var i = 0; i < visibleItems.length; i++) { | |
1590 var index = this.currentList_.getIndexOfListItem(visibleItems[i]); | |
1591 var entry = this.directoryModel_.getFileList().item(index); | |
1592 // The following check is a workaround for the bug in list: sometimes item | |
1593 // does not have listIndex, and therefore is not found in the list. | |
1594 if (entry) visibleEntries.push(entry); | |
1595 } | |
1596 this.metadataCache_.get(visibleEntries, 'thumbnail', null); | |
1597 }; | |
1598 | |
1599 /** | |
1600 * @private | |
1601 */ | |
1602 FileManager.prototype.dailyUpdateModificationTime_ = function() { | |
1603 var fileList = this.directoryModel_.getFileList(); | |
1604 var urls = []; | |
1605 for (var i = 0; i < fileList.length; i++) { | |
1606 urls.push(fileList.item(i).toURL()); | |
1607 } | |
1608 this.metadataCache_.get( | |
1609 fileList.slice(), 'filesystem', | |
1610 this.updateMetadataInUI_.bind(this, 'filesystem', urls)); | |
1611 | |
1612 setTimeout(this.dailyUpdateModificationTime_.bind(this), | |
1613 MILLISECONDS_IN_DAY); | |
1614 }; | |
1615 | |
1616 /** | |
1617 * @param {string} type Type of metadata changed. | |
1618 * @param {Array.<string>} urls Array of urls. | |
1619 * @param {Object.<string, Object>} props Map from entry URLs to metadata | |
1620 * props. | |
1621 * @private | |
1622 */ | |
1623 FileManager.prototype.updateMetadataInUI_ = function( | |
1624 type, urls, properties) { | |
1625 var propertyByUrl = urls.reduce(function(map, url, index) { | |
1626 map[url] = properties[index]; | |
1627 return map; | |
1628 }, {}); | |
1629 | |
1630 if (this.listType_ == FileManager.ListType.DETAIL) | |
1631 this.table_.updateListItemsMetadata(type, propertyByUrl); | |
1632 else | |
1633 this.grid_.updateListItemsMetadata(type, propertyByUrl); | |
1634 // TODO: update bottom panel thumbnails. | |
1635 }; | |
1636 | |
1637 /** | |
1638 * Restore the item which is being renamed while refreshing the file list. Do | |
1639 * nothing if no item is being renamed or such an item disappeared. | |
1640 * | |
1641 * While refreshing file list it gets repopulated with new file entries. | |
1642 * There is not a big difference whether DOM items stay the same or not. | |
1643 * Except for the item that the user is renaming. | |
1644 * | |
1645 * @private | |
1646 */ | |
1647 FileManager.prototype.restoreItemBeingRenamed_ = function() { | |
1648 if (!this.isRenamingInProgress()) | |
1649 return; | |
1650 | |
1651 var dm = this.directoryModel_; | |
1652 var leadIndex = dm.getFileListSelection().leadIndex; | |
1653 if (leadIndex < 0) | |
1654 return; | |
1655 | |
1656 var leadEntry = dm.getFileList().item(leadIndex); | |
1657 if (this.renameInput_.currentEntry.fullPath != leadEntry.fullPath) | |
1658 return; | |
1659 | |
1660 var leadListItem = this.findListItemForNode_(this.renameInput_); | |
1661 if (this.currentList_ == this.table_.list) { | |
1662 this.table_.updateFileMetadata(leadListItem, leadEntry); | |
1663 } | |
1664 this.currentList_.restoreLeadItem(leadListItem); | |
1665 }; | |
1666 | |
1667 /** | |
1668 * @return {boolean} True if the current directory content is from Google | |
1669 * Drive. | |
1670 */ | |
1671 FileManager.prototype.isOnDrive = function() { | |
1672 var rootType = this.directoryModel_.getCurrentRootType(); | |
1673 return rootType === RootType.DRIVE || | |
1674 rootType === RootType.DRIVE_SHARED_WITH_ME || | |
1675 rootType === RootType.DRIVE_RECENT || | |
1676 rootType === RootType.DRIVE_OFFLINE; | |
1677 }; | |
1678 | |
1679 /** | |
1680 * Overrides default handling for clicks on hyperlinks. | |
1681 * In a packaged apps links with targer='_blank' open in a new tab by | |
1682 * default, other links do not open at all. | |
1683 * | |
1684 * @param {Event} event Click event. | |
1685 * @private | |
1686 */ | |
1687 FileManager.prototype.onExternalLinkClick_ = function(event) { | |
1688 if (event.target.tagName != 'A' || !event.target.href) | |
1689 return; | |
1690 | |
1691 if (this.dialogType != DialogType.FULL_PAGE) | |
1692 this.onCancel_(); | |
1693 }; | |
1694 | |
1695 /** | |
1696 * Task combobox handler. | |
1697 * | |
1698 * @param {Object} event Event containing task which was clicked. | |
1699 * @private | |
1700 */ | |
1701 FileManager.prototype.onTaskItemClicked_ = function(event) { | |
1702 var selection = this.getSelection(); | |
1703 if (!selection.tasks) return; | |
1704 | |
1705 if (event.item.task) { | |
1706 // Task field doesn't exist on change-default dropdown item. | |
1707 selection.tasks.execute(event.item.task.taskId); | |
1708 } else { | |
1709 var extensions = []; | |
1710 | |
1711 for (var i = 0; i < selection.urls.length; i++) { | |
1712 var match = /\.(\w+)$/g.exec(selection.urls[i]); | |
1713 if (match) { | |
1714 var ext = match[1].toUpperCase(); | |
1715 if (extensions.indexOf(ext) == -1) { | |
1716 extensions.push(ext); | |
1717 } | |
1718 } | |
1719 } | |
1720 | |
1721 var format = ''; | |
1722 | |
1723 if (extensions.length == 1) { | |
1724 format = extensions[0]; | |
1725 } | |
1726 | |
1727 // Change default was clicked. We should open "change default" dialog. | |
1728 selection.tasks.showTaskPicker(this.defaultTaskPicker, | |
1729 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'), | |
1730 strf('CHANGE_DEFAULT_CAPTION', format), | |
1731 this.onDefaultTaskDone_.bind(this)); | |
1732 } | |
1733 }; | |
1734 | |
1735 | |
1736 /** | |
1737 * Sets the given task as default, when this task is applicable. | |
1738 * | |
1739 * @param {Object} task Task to set as default. | |
1740 * @private | |
1741 */ | |
1742 FileManager.prototype.onDefaultTaskDone_ = function(task) { | |
1743 // TODO(dgozman): move this method closer to tasks. | |
1744 var selection = this.getSelection(); | |
1745 chrome.fileBrowserPrivate.setDefaultTask(task.taskId, | |
1746 selection.urls, selection.mimeTypes); | |
1747 selection.tasks = new FileTasks(this); | |
1748 selection.tasks.init(selection.urls, selection.mimeTypes); | |
1749 selection.tasks.display(this.taskItems_); | |
1750 this.refreshCurrentDirectoryMetadata_(); | |
1751 this.selectionHandler_.onFileSelectionChanged(); | |
1752 }; | |
1753 | |
1754 /** | |
1755 * @private | |
1756 */ | |
1757 FileManager.prototype.onPreferencesChanged_ = function() { | |
1758 var self = this; | |
1759 this.getPreferences_(function(prefs) { | |
1760 self.initDateTimeFormatters_(); | |
1761 self.refreshCurrentDirectoryMetadata_(); | |
1762 | |
1763 if (prefs.cellularDisabled) | |
1764 self.syncButton.setAttribute('checked', ''); | |
1765 else | |
1766 self.syncButton.removeAttribute('checked'); | |
1767 | |
1768 if (self.hostedButton.hasAttribute('checked') != | |
1769 prefs.hostedFilesDisabled && self.isOnDrive()) { | |
1770 self.directoryModel_.rescan(); | |
1771 } | |
1772 | |
1773 if (!prefs.hostedFilesDisabled) | |
1774 self.hostedButton.setAttribute('checked', ''); | |
1775 else | |
1776 self.hostedButton.removeAttribute('checked'); | |
1777 }, | |
1778 true /* refresh */); | |
1779 }; | |
1780 | |
1781 FileManager.prototype.onDriveConnectionChanged_ = function() { | |
1782 var connection = this.volumeManager_.getDriveConnectionState(); | |
1783 if (this.commandHandler) | |
1784 this.commandHandler.updateAvailability(); | |
1785 if (this.dialogContainer_) | |
1786 this.dialogContainer_.setAttribute('connection', connection.type); | |
1787 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR); | |
1788 this.suggestAppsDialog.onDriveConnectionChanged(connection.type); | |
1789 }; | |
1790 | |
1791 /** | |
1792 * Get the metered status of Drive connection. | |
1793 * | |
1794 * @return {boolean} Returns true if drive should limit the traffic because | |
1795 * the connection is metered and the 'disable-sync-on-metered' setting is | |
1796 * enabled. Otherwise, returns false. | |
1797 */ | |
1798 FileManager.prototype.isDriveOnMeteredConnection = function() { | |
1799 var connection = this.volumeManager_.getDriveConnectionState(); | |
1800 return connection.type == util.DriveConnectionType.METERED; | |
1801 }; | |
1802 | |
1803 /** | |
1804 * Get the online/offline status of drive. | |
1805 * | |
1806 * @return {boolean} Returns true if the connection is offline. Otherwise, | |
1807 * returns false. | |
1808 */ | |
1809 FileManager.prototype.isDriveOffline = function() { | |
1810 var connection = this.volumeManager_.getDriveConnectionState(); | |
1811 return connection.type == util.DriveConnectionType.OFFLINE; | |
1812 }; | |
1813 | |
1814 FileManager.prototype.isOnReadonlyDirectory = function() { | |
1815 return this.directoryModel_.isReadOnly(); | |
1816 }; | |
1817 | |
1818 /** | |
1819 * @param {Event} Unmount event. | |
1820 * @private | |
1821 */ | |
1822 FileManager.prototype.onExternallyUnmounted_ = function(event) { | |
1823 if (event.mountPath == this.directoryModel_.getCurrentRootPath()) { | |
1824 if (this.closeOnUnmount_) { | |
1825 // If the file manager opened automatically when a usb drive inserted, | |
1826 // user have never changed current volume (that implies the current | |
1827 // directory is still on the device) then close this window. | |
1828 window.close(); | |
1829 } | |
1830 } | |
1831 }; | |
1832 | |
1833 /** | |
1834 * Show a modal-like file viewer/editor on top of the File Manager UI. | |
1835 * | |
1836 * @param {HTMLElement} popup Popup element. | |
1837 * @param {function} closeCallback Function to call after the popup is closed. | |
1838 * | |
1839 * @private | |
1840 */ | |
1841 FileManager.prototype.openFilePopup_ = function(popup, closeCallback) { | |
1842 this.closeFilePopup_(); | |
1843 this.filePopup_ = popup; | |
1844 this.filePopupCloseCallback_ = closeCallback; | |
1845 this.dialogDom_.appendChild(this.filePopup_); | |
1846 this.filePopup_.focus(); | |
1847 this.document_.body.setAttribute('overlay-visible', ''); | |
1848 this.document_.querySelector('#iframe-drag-area').hidden = false; | |
1849 }; | |
1850 | |
1851 /** | |
1852 * @private | |
1853 */ | |
1854 FileManager.prototype.closeFilePopup_ = function() { | |
1855 if (this.filePopup_) { | |
1856 this.document_.body.removeAttribute('overlay-visible'); | |
1857 this.document_.querySelector('#iframe-drag-area').hidden = true; | |
1858 // The window resize would not be processed properly while the relevant | |
1859 // divs had 'display:none', force resize after the layout fired. | |
1860 setTimeout(this.onResize_.bind(this), 0); | |
1861 if (this.filePopup_.contentWindow && | |
1862 this.filePopup_.contentWindow.unload) { | |
1863 this.filePopup_.contentWindow.unload(); | |
1864 } | |
1865 | |
1866 if (this.filePopupCloseCallback_) { | |
1867 this.filePopupCloseCallback_(); | |
1868 this.filePopupCloseCallback_ = null; | |
1869 } | |
1870 | |
1871 // These operations have to be in the end, otherwise v8 crashes on an | |
1872 // assert. See: crbug.com/224174. | |
1873 this.dialogDom_.removeChild(this.filePopup_); | |
1874 this.filePopup_ = null; | |
1875 } | |
1876 }; | |
1877 | |
1878 FileManager.prototype.getAllUrlsInCurrentDirectory = function() { | |
1879 var urls = []; | |
1880 var fileList = this.directoryModel_.getFileList(); | |
1881 for (var i = 0; i != fileList.length; i++) { | |
1882 urls.push(fileList.item(i).toURL()); | |
1883 } | |
1884 return urls; | |
1885 }; | |
1886 | |
1887 FileManager.prototype.isRenamingInProgress = function() { | |
1888 return !!this.renameInput_.currentEntry; | |
1889 }; | |
1890 | |
1891 /** | |
1892 * @private | |
1893 */ | |
1894 FileManager.prototype.focusCurrentList_ = function() { | |
1895 if (this.listType_ == FileManager.ListType.DETAIL) | |
1896 this.table_.focus(); | |
1897 else // this.listType_ == FileManager.ListType.THUMBNAIL) | |
1898 this.grid_.focus(); | |
1899 }; | |
1900 | |
1901 /** | |
1902 * Return full path of the current directory or null. | |
1903 * @return {?string} The full path of the current directory. | |
1904 */ | |
1905 FileManager.prototype.getCurrentDirectory = function() { | |
1906 return this.directoryModel_ && this.directoryModel_.getCurrentDirPath(); | |
1907 }; | |
1908 | |
1909 /** | |
1910 * Return URL of the current directory or null. | |
1911 * @return {string} URL representing the current directory. | |
1912 */ | |
1913 FileManager.prototype.getCurrentDirectoryURL = function() { | |
1914 return this.directoryModel_ && | |
1915 this.directoryModel_.getCurrentDirectoryURL(); | |
1916 }; | |
1917 | |
1918 /** | |
1919 * Return DirectoryEntry of the current directory or null. | |
1920 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns | |
1921 * null if the directory model is not ready or the current directory is | |
1922 * not set. | |
1923 */ | |
1924 FileManager.prototype.getCurrentDirectoryEntry = function() { | |
1925 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry(); | |
1926 }; | |
1927 | |
1928 /** | |
1929 * Deletes the selected file and directories recursively. | |
1930 */ | |
1931 FileManager.prototype.deleteSelection = function() { | |
1932 // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364 | |
1933 var entries = this.getSelection().entries; | |
1934 var message = entries.length == 1 ? | |
1935 strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) : | |
1936 strf('GALLERY_CONFIRM_DELETE_SOME', entries.length); | |
1937 this.confirm.show(message, function() { | |
1938 this.fileOperationManager_.deleteEntries(entries); | |
1939 }.bind(this)); | |
1940 }; | |
1941 | |
1942 /** | |
1943 * Shows the share dialog for the selected file or directory. | |
1944 */ | |
1945 FileManager.prototype.shareSelection = function() { | |
1946 var entries = this.getSelection().entries; | |
1947 if (entries.length != 1) { | |
1948 console.warn('Unable to share multiple items at once.'); | |
1949 return; | |
1950 } | |
1951 // Add the overlapped class to prevent the applicaiton window from | |
1952 // captureing mouse events. | |
1953 this.shareDialog_.show(entries[0], function(result) { | |
1954 if (result == ShareDialog.Result.NETWORK_ERROR) | |
1955 this.error.show(str('SHARE_ERROR')); | |
1956 }.bind(this)); | |
1957 }; | |
1958 | |
1959 /** | |
1960 * Creates a folder shortcut. | |
1961 * @param {string} path A shortcut which refers to |path| to be created. | |
1962 */ | |
1963 FileManager.prototype.createFolderShortcut = function(path) { | |
1964 // Duplicate entry. | |
1965 if (this.folderShortcutExists(path)) | |
1966 return; | |
1967 | |
1968 this.folderShortcutsModel_.add(path); | |
1969 }; | |
1970 | |
1971 /** | |
1972 * Checkes if the shortcut which refers to the given folder exists or not. | |
1973 * @param {string} path Path of the folder to be checked. | |
1974 */ | |
1975 FileManager.prototype.folderShortcutExists = function(path) { | |
1976 return this.folderShortcutsModel_.exists(path); | |
1977 }; | |
1978 | |
1979 /** | |
1980 * Removes the folder shortcut. | |
1981 * @param {string} path The shortcut which refers to |path| is to be removed. | |
1982 */ | |
1983 FileManager.prototype.removeFolderShortcut = function(path) { | |
1984 this.folderShortcutsModel_.remove(path); | |
1985 }; | |
1986 | |
1987 /** | |
1988 * Blinks the selection. Used to give feedback when copying or cutting the | |
1989 * selection. | |
1990 */ | |
1991 FileManager.prototype.blinkSelection = function() { | |
1992 var selection = this.getSelection(); | |
1993 if (!selection || selection.totalCount == 0) | |
1994 return; | |
1995 | |
1996 for (var i = 0; i < selection.entries.length; i++) { | |
1997 var selectedIndex = selection.indexes[i]; | |
1998 var listItem = this.currentList_.getListItemByIndex(selectedIndex); | |
1999 if (listItem) | |
2000 this.blinkListItem_(listItem); | |
2001 } | |
2002 }; | |
2003 | |
2004 /** | |
2005 * @param {Element} listItem List item element. | |
2006 * @private | |
2007 */ | |
2008 FileManager.prototype.blinkListItem_ = function(listItem) { | |
2009 listItem.classList.add('blink'); | |
2010 setTimeout(function() { | |
2011 listItem.classList.remove('blink'); | |
2012 }, 100); | |
2013 }; | |
2014 | |
2015 /** | |
2016 * @private | |
2017 */ | |
2018 FileManager.prototype.selectDefaultPathInFilenameInput_ = function() { | |
2019 var input = this.filenameInput_; | |
2020 input.focus(); | |
2021 var selectionEnd = input.value.lastIndexOf('.'); | |
2022 if (selectionEnd == -1) { | |
2023 input.select(); | |
2024 } else { | |
2025 input.selectionStart = 0; | |
2026 input.selectionEnd = selectionEnd; | |
2027 } | |
2028 // Clear, so we never do this again. | |
2029 this.defaultPath = ''; | |
2030 }; | |
2031 | |
2032 /** | |
2033 * Handles mouse click or tap. | |
2034 * | |
2035 * @param {Event} event The click event. | |
2036 * @private | |
2037 */ | |
2038 FileManager.prototype.onDetailClick_ = function(event) { | |
2039 if (this.isRenamingInProgress()) { | |
2040 // Don't pay attention to clicks during a rename. | |
2041 return; | |
2042 } | |
2043 | |
2044 var listItem = this.findListItemForEvent_(event); | |
2045 var selection = this.getSelection(); | |
2046 if (!listItem || !listItem.selected || selection.totalCount != 1) { | |
2047 return; | |
2048 } | |
2049 | |
2050 // React on double click, but only if both clicks hit the same item. | |
2051 // TODO(mtomasz): Simplify it, and use a double click handler if possible. | |
2052 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined; | |
2053 this.lastClickedItem_ = listItem; | |
2054 | |
2055 if (event.detail != clickNumber) | |
2056 return; | |
2057 | |
2058 var entry = selection.entries[0]; | |
2059 if (entry.isDirectory) { | |
2060 this.onDirectoryAction_(entry); | |
2061 } else { | |
2062 this.dispatchSelectionAction_(); | |
2063 } | |
2064 }; | |
2065 | |
2066 /** | |
2067 * @private | |
2068 */ | |
2069 FileManager.prototype.dispatchSelectionAction_ = function() { | |
2070 if (this.dialogType == DialogType.FULL_PAGE) { | |
2071 var selection = this.getSelection(); | |
2072 var tasks = selection.tasks; | |
2073 var urls = selection.urls; | |
2074 var mimeTypes = selection.mimeTypes; | |
2075 if (tasks) { | |
2076 tasks.executeDefault(function(result) { | |
2077 if (result) | |
2078 return; | |
2079 | |
2080 var showAlert = function() { | |
2081 var filename = decodeURIComponent(urls[0]); | |
2082 if (filename.indexOf('/') != -1) | |
2083 filename = filename.substr(filename.lastIndexOf('/') + 1); | |
2084 var extension = filename.lastIndexOf('.') != -1 ? | |
2085 filename.substr(filename.lastIndexOf('.') + 1) : ''; | |
2086 var mimeType = mimeTypes && mimeTypes[0]; | |
2087 | |
2088 var messageString = | |
2089 extension == 'exe' ? 'NO_ACTION_FOR_EXECUTABLE' : | |
2090 'NO_ACTION_FOR_FILE'; | |
2091 var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType); | |
2092 var text = loadTimeData.getStringF( | |
2093 messageString, | |
2094 webStoreUrl, | |
2095 FileTasks.NO_ACTION_FOR_FILE_URL); | |
2096 this.alert.showHtml(filename, text, function() {}); | |
2097 }.bind(this); | |
2098 | |
2099 // TODO(yoshiki): Remove the flag when the feature is launched. | |
2100 if (!this.enableExperimentalWebstoreIntegration_) { | |
2101 showAlert(); | |
2102 return; | |
2103 } | |
2104 | |
2105 if (this.isDriveOffline()) { | |
2106 showAlert(); | |
2107 return; | |
2108 } | |
2109 | |
2110 this.openSuggestAppsDialog_(urls, | |
2111 // Success callback. | |
2112 function() { | |
2113 var tasks = new FileTasks(this); | |
2114 tasks.init(urls, mimeTypes); | |
2115 tasks.executeDefault(); | |
2116 }.bind(this), | |
2117 // Cancelled callback. | |
2118 function() {}, | |
2119 // Failure callback. | |
2120 showAlert); | |
2121 }.bind(this)); | |
2122 } | |
2123 return true; | |
2124 } | |
2125 if (!this.okButton_.disabled) { | |
2126 this.onOk_(); | |
2127 return true; | |
2128 } | |
2129 return false; | |
2130 }; | |
2131 | |
2132 /** | |
2133 * Opens the suggest file dialog. | |
2134 * | |
2135 * @param {Array.<string>} urls List of URLs of files. | |
2136 * @param {function()} onSuccess Success callback. | |
2137 * @param {function()} onCancelled User-cancelled callback. | |
2138 * @param {function()} onFailure Failure callback. | |
2139 * @private | |
2140 */ | |
2141 FileManager.prototype.openSuggestAppsDialog_ = | |
2142 function(urls, onSuccess, onCancelled, onFailure) { | |
2143 if (!urls || urls.length != 1) { | |
2144 onFailure(); | |
2145 return; | |
2146 } | |
2147 | |
2148 this.metadataCache_.get(urls, 'drive', function(props) { | |
2149 if (!props || !props[0] || !props[0].contentMimeType) { | |
2150 onFailure(); | |
2151 return; | |
2152 } | |
2153 | |
2154 var filename = util.extractFilePath(urls[0]); | |
2155 var extension = PathUtil.extractExtension(filename); | |
2156 var mime = props[0].contentMimeType; | |
2157 | |
2158 // Returns with failure if the file has neither extension nor mime. | |
2159 if (!extension || !mime) { | |
2160 onFailure(); | |
2161 return; | |
2162 } | |
2163 | |
2164 this.suggestAppsDialog.show( | |
2165 extension, mime, | |
2166 function(result) { | |
2167 switch (result) { | |
2168 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL: | |
2169 onSuccess(); | |
2170 break; | |
2171 case SuggestAppsDialog.Result.FAILED: | |
2172 onFailure(); | |
2173 break; | |
2174 default: | |
2175 onCancelled(); | |
2176 } | |
2177 }); | |
2178 }.bind(this)); | |
2179 }; | |
2180 | |
2181 /** | |
2182 * Called when a dialog is shown or hidden. | |
2183 * @param {boolean} flag True if a dialog is shown, false if hidden. */ | |
2184 FileManager.prototype.onDialogShownOrHidden = function(show) { | |
2185 // Set/unset a flag to disable dragging on the title area. | |
2186 this.dialogContainer_.classList.toggle('disable-header-drag', show); | |
2187 }; | |
2188 | |
2189 /** | |
2190 * Executes directory action (i.e. changes directory). | |
2191 * | |
2192 * @param {DirectoryEntry} entry Directory entry to which directory should be | |
2193 * changed. | |
2194 * @private | |
2195 */ | |
2196 FileManager.prototype.onDirectoryAction_ = function(entry) { | |
2197 return this.directoryModel_.changeDirectory(entry.fullPath); | |
2198 }; | |
2199 | |
2200 /** | |
2201 * Update the window title. | |
2202 * @private | |
2203 */ | |
2204 FileManager.prototype.updateTitle_ = function() { | |
2205 if (this.dialogType != DialogType.FULL_PAGE) | |
2206 return; | |
2207 | |
2208 var path = this.getCurrentDirectory(); | |
2209 var rootPath = PathUtil.getRootPath(path); | |
2210 this.document_.title = PathUtil.getRootLabel(rootPath) + | |
2211 path.substring(rootPath.length); | |
2212 }; | |
2213 | |
2214 /** | |
2215 * Update the gear menu. | |
2216 * @private | |
2217 */ | |
2218 FileManager.prototype.updateGearMenu_ = function() { | |
2219 var hideItemsForDrive = !this.isOnDrive(); | |
2220 this.syncButton.hidden = hideItemsForDrive; | |
2221 this.hostedButton.hidden = hideItemsForDrive; | |
2222 this.document_.getElementById('drive-separator').hidden = | |
2223 hideItemsForDrive; | |
2224 | |
2225 // If volume has changed, then fetch remaining space data. | |
2226 if (this.previousRootUrl_ != this.directoryModel_.getCurrentMountPointUrl()) | |
2227 this.refreshRemainingSpace_(true); // Show loading caption. | |
2228 | |
2229 this.previousRootUrl_ = this.directoryModel_.getCurrentMountPointUrl(); | |
2230 }; | |
2231 | |
2232 /** | |
2233 * Refreshes space info of the current volume. | |
2234 * @param {boolean} showLoadingCaption Whether show loading caption or not. | |
2235 * @private | |
2236 */ | |
2237 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) { | |
2238 var volumeSpaceInfoLabel = | |
2239 this.dialogDom_.querySelector('#volume-space-info-label'); | |
2240 var volumeSpaceInnerBar = | |
2241 this.dialogDom_.querySelector('#volume-space-info-bar'); | |
2242 var volumeSpaceOuterBar = | |
2243 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode; | |
2244 | |
2245 volumeSpaceInnerBar.setAttribute('pending', ''); | |
2246 | |
2247 if (showLoadingCaption) { | |
2248 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO'); | |
2249 volumeSpaceInnerBar.style.width = '100%'; | |
2250 } | |
2251 | |
2252 var currentMountPointUrl = this.directoryModel_.getCurrentMountPointUrl(); | |
2253 chrome.fileBrowserPrivate.getSizeStats( | |
2254 currentMountPointUrl, function(result) { | |
2255 if (this.directoryModel_.getCurrentMountPointUrl() != | |
2256 currentMountPointUrl) | |
2257 return; | |
2258 updateSpaceInfo(result, | |
2259 volumeSpaceInnerBar, | |
2260 volumeSpaceInfoLabel, | |
2261 volumeSpaceOuterBar); | |
2262 }.bind(this)); | |
2263 }; | |
2264 | |
2265 /** | |
2266 * Update the UI when the current directory changes. | |
2267 * | |
2268 * @param {Event} event The directory-changed event. | |
2269 * @private | |
2270 */ | |
2271 FileManager.prototype.onDirectoryChanged_ = function(event) { | |
2272 this.selectionHandler_.onFileSelectionChanged(); | |
2273 this.ui_.searchBox.clear(); | |
2274 util.updateAppState(this.getCurrentDirectory()); | |
2275 | |
2276 // If the current directory is moved from the device's volume, do not | |
2277 // automatically close the window on device removal. | |
2278 if (event.previousDirEntry && | |
2279 PathUtil.getRootPath(event.previousDirEntry.fullPath) != | |
2280 PathUtil.getRootPath(event.newDirEntry.fullPath)) | |
2281 this.closeOnUnmount_ = false; | |
2282 | |
2283 if (this.commandHandler) | |
2284 this.commandHandler.updateAvailability(); | |
2285 this.updateUnformattedDriveStatus_(); | |
2286 this.updateTitle_(); | |
2287 this.updateGearMenu_(); | |
2288 this.previewPanel_.currentPath_ = this.getCurrentDirectory(); | |
2289 }; | |
2290 | |
2291 // TODO(haruki): Rename this method. "Drive" here does not refer | |
2292 // "Google Drive". | |
2293 FileManager.prototype.updateUnformattedDriveStatus_ = function() { | |
2294 var volumeInfo = this.volumeManager_.getVolumeInfo( | |
2295 PathUtil.getRootPath(this.directoryModel_.getCurrentRootPath())); | |
2296 | |
2297 if (volumeInfo && volumeInfo.error) { | |
2298 this.dialogDom_.setAttribute('unformatted', ''); | |
2299 | |
2300 var errorNode = this.dialogDom_.querySelector('#format-panel > .error'); | |
2301 if (volumeInfo.error == util.VolumeError.UNSUPPORTED_FILESYSTEM) { | |
2302 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING'); | |
2303 } else { | |
2304 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING'); | |
2305 } | |
2306 | |
2307 // Update 'canExecute' for format command so the format button's disabled | |
2308 // property is properly set. | |
2309 if (this.commandHandler) | |
2310 this.commandHandler.updateAvailability(); | |
2311 } else { | |
2312 this.dialogDom_.removeAttribute('unformatted'); | |
2313 } | |
2314 }; | |
2315 | |
2316 FileManager.prototype.findListItemForEvent_ = function(event) { | |
2317 return this.findListItemForNode_(event.touchedElement || event.srcElement); | |
2318 }; | |
2319 | |
2320 FileManager.prototype.findListItemForNode_ = function(node) { | |
2321 var item = this.currentList_.getListItemAncestor(node); | |
2322 // TODO(serya): list should check that. | |
2323 return item && this.currentList_.isItem(item) ? item : null; | |
2324 }; | |
2325 | |
2326 /** | |
2327 * Unload handler for the page. May be called manually for the file picker | |
2328 * dialog, because it closes by calling extension API functions that do not | |
2329 * return. | |
2330 * | |
2331 * @private | |
2332 */ | |
2333 FileManager.prototype.onUnload_ = function() { | |
2334 if (this.directoryModel_) | |
2335 this.directoryModel_.dispose(); | |
2336 if (this.volumeManager_) | |
2337 this.volumeManager_.dispose(); | |
2338 if (this.filePopup_ && | |
2339 this.filePopup_.contentWindow && | |
2340 this.filePopup_.contentWindow.unload) | |
2341 this.filePopup_.contentWindow.unload(true /* exiting */); | |
2342 if (this.butterBar_) | |
2343 this.butterBar_.dispose(); | |
2344 if (this.fileOperationManager_) { | |
2345 if (this.onCopyProgressBound_) { | |
2346 this.fileOperationManager_.removeEventListener( | |
2347 'copy-progress', this.onCopyProgressBound_); | |
2348 } | |
2349 if (this.onEntryChangedBound_) { | |
2350 this.fileOperationManager_.removeEventListener( | |
2351 'entry-changed', this.onEntryChangedBound_); | |
2352 } | |
2353 } | |
2354 }; | |
2355 | |
2356 FileManager.prototype.initiateRename = function() { | |
2357 var item = this.currentList_.ensureLeadItemExists(); | |
2358 if (!item) | |
2359 return; | |
2360 var label = item.querySelector('.filename-label'); | |
2361 var input = this.renameInput_; | |
2362 | |
2363 input.value = label.textContent; | |
2364 label.parentNode.setAttribute('renaming', ''); | |
2365 label.parentNode.appendChild(input); | |
2366 input.focus(); | |
2367 var selectionEnd = input.value.lastIndexOf('.'); | |
2368 if (selectionEnd == -1) { | |
2369 input.select(); | |
2370 } else { | |
2371 input.selectionStart = 0; | |
2372 input.selectionEnd = selectionEnd; | |
2373 } | |
2374 | |
2375 // This has to be set late in the process so we don't handle spurious | |
2376 // blur events. | |
2377 input.currentEntry = this.currentList_.dataModel.item(item.listIndex); | |
2378 }; | |
2379 | |
2380 /** | |
2381 * @type {Event} Key event. | |
2382 * @private | |
2383 */ | |
2384 FileManager.prototype.onRenameInputKeyDown_ = function(event) { | |
2385 if (!this.isRenamingInProgress()) | |
2386 return; | |
2387 | |
2388 // Do not move selection or lead item in list during rename. | |
2389 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') { | |
2390 event.stopPropagation(); | |
2391 } | |
2392 | |
2393 switch (util.getKeyModifiers(event) + event.keyCode) { | |
2394 case '27': // Escape | |
2395 this.cancelRename_(); | |
2396 event.preventDefault(); | |
2397 break; | |
2398 | |
2399 case '13': // Enter | |
2400 this.commitRename_(); | |
2401 event.preventDefault(); | |
2402 break; | |
2403 } | |
2404 }; | |
2405 | |
2406 /** | |
2407 * @type {Event} Blur event. | |
2408 * @private | |
2409 */ | |
2410 FileManager.prototype.onRenameInputBlur_ = function(event) { | |
2411 if (this.isRenamingInProgress() && !this.renameInput_.validation_) | |
2412 this.commitRename_(); | |
2413 }; | |
2414 | |
2415 /** | |
2416 * @private | |
2417 */ | |
2418 FileManager.prototype.commitRename_ = function() { | |
2419 var input = this.renameInput_; | |
2420 var entry = input.currentEntry; | |
2421 var newName = input.value; | |
2422 | |
2423 if (newName == entry.name) { | |
2424 this.cancelRename_(); | |
2425 return; | |
2426 } | |
2427 | |
2428 var nameNode = this.findListItemForNode_(this.renameInput_). | |
2429 querySelector('.filename-label'); | |
2430 | |
2431 input.validation_ = true; | |
2432 var validationDone = function(valid) { | |
2433 input.validation_ = false; | |
2434 // Alert dialog restores focus unless the item removed from DOM. | |
2435 if (this.document_.activeElement != input) | |
2436 this.cancelRename_(); | |
2437 if (!valid) | |
2438 return; | |
2439 | |
2440 // Validation succeeded. Do renaming. | |
2441 | |
2442 this.cancelRename_(); | |
2443 // Optimistically apply new name immediately to avoid flickering in | |
2444 // case of success. | |
2445 nameNode.textContent = newName; | |
2446 | |
2447 util.rename( | |
2448 entry, newName, | |
2449 function(newEntry) { | |
2450 this.directoryModel_.onRenameEntry(entry, newEntry); | |
2451 }.bind(this), | |
2452 function(error) { | |
2453 // Write back to the old name. | |
2454 nameNode.textContent = entry.name; | |
2455 | |
2456 // Show error dialog. | |
2457 var message; | |
2458 if (error.code == FileError.PATH_EXISTS_ERR || | |
2459 error.code == FileError.TYPE_MISMATCH_ERR) { | |
2460 // Check the existing entry is file or not. | |
2461 // 1) If the entry is a file: | |
2462 // a) If we get PATH_EXISTS_ERR, a file exists. | |
2463 // b) If we get TYPE_MISMATCH_ERR, a directory exists. | |
2464 // 2) If the entry is a directory: | |
2465 // a) If we get PATH_EXISTS_ERR, a directory exists. | |
2466 // b) If we get TYPE_MISMATCH_ERR, a file exists. | |
2467 message = strf( | |
2468 (entry.isFile && error.code == FileError.PATH_EXISTS_ERR) || | |
2469 (!entry.isFile && error.code == FileError.TYPE_MISMATCH_ERR) ? | |
2470 'FILE_ALREADY_EXISTS' : | |
2471 'DIRECTORY_ALREADY_EXISTS', | |
2472 newName); | |
2473 } else { | |
2474 message = strf('ERROR_RENAMING', entry.name, | |
2475 util.getFileErrorString(err.code)); | |
2476 } | |
2477 | |
2478 this.alert.show(message); | |
2479 }.bind(this)); | |
2480 }; | |
2481 | |
2482 // TODO(haruki): this.getCurrentDirectoryURL() might not return the actual | |
2483 // parent if the directory content is a search result. Fix it to do proper | |
2484 // validation. | |
2485 this.validateFileName_(this.getCurrentDirectoryURL(), | |
2486 newName, | |
2487 validationDone.bind(this)); | |
2488 }; | |
2489 | |
2490 /** | |
2491 * @private | |
2492 */ | |
2493 FileManager.prototype.cancelRename_ = function() { | |
2494 this.renameInput_.currentEntry = null; | |
2495 | |
2496 var parent = this.renameInput_.parentNode; | |
2497 if (parent) { | |
2498 parent.removeAttribute('renaming'); | |
2499 parent.removeChild(this.renameInput_); | |
2500 } | |
2501 }; | |
2502 | |
2503 /** | |
2504 * @param {Event} Key event. | |
2505 * @private | |
2506 */ | |
2507 FileManager.prototype.onFilenameInputKeyDown_ = function(event) { | |
2508 var enabled = this.selectionHandler_.updateOkButton(); | |
2509 if (enabled && | |
2510 (util.getKeyModifiers(event) + event.keyCode) == '13' /* Enter */) | |
2511 this.onOk_(); | |
2512 }; | |
2513 | |
2514 /** | |
2515 * @param {Event} Focus event. | |
2516 * @private | |
2517 */ | |
2518 FileManager.prototype.onFilenameInputFocus_ = function(event) { | |
2519 var input = this.filenameInput_; | |
2520 | |
2521 // On focus we want to select everything but the extension, but | |
2522 // Chrome will select-all after the focus event completes. We | |
2523 // schedule a timeout to alter the focus after that happens. | |
2524 setTimeout(function() { | |
2525 var selectionEnd = input.value.lastIndexOf('.'); | |
2526 if (selectionEnd == -1) { | |
2527 input.select(); | |
2528 } else { | |
2529 input.selectionStart = 0; | |
2530 input.selectionEnd = selectionEnd; | |
2531 } | |
2532 }, 0); | |
2533 }; | |
2534 | |
2535 /** | |
2536 * @private | |
2537 */ | |
2538 FileManager.prototype.onScanStarted_ = function() { | |
2539 if (this.scanInProgress_) { | |
2540 this.table_.list.endBatchUpdates(); | |
2541 this.grid_.endBatchUpdates(); | |
2542 } | |
2543 | |
2544 if (this.commandHandler) | |
2545 this.commandHandler.updateAvailability(); | |
2546 this.table_.list.startBatchUpdates(); | |
2547 this.grid_.startBatchUpdates(); | |
2548 this.scanInProgress_ = true; | |
2549 | |
2550 this.scanUpdatedAtLeastOnceOrCompleted_ = false; | |
2551 if (this.scanCompletedTimer_) { | |
2552 clearTimeout(this.scanCompletedTimer_); | |
2553 this.scanCompletedTimer_ = null; | |
2554 } | |
2555 | |
2556 if (this.scanUpdatedTimer_) { | |
2557 clearTimeout(this.scanUpdatedTimer_); | |
2558 this.scanUpdatedTimer_ = null; | |
2559 } | |
2560 | |
2561 if (this.spinner_.hidden) { | |
2562 this.cancelSpinnerTimeout_(); | |
2563 this.showSpinnerTimeout_ = | |
2564 setTimeout(this.showSpinner_.bind(this, true), 500); | |
2565 } | |
2566 }; | |
2567 | |
2568 /** | |
2569 * @private | |
2570 */ | |
2571 FileManager.prototype.onScanCompleted_ = function() { | |
2572 if (!this.scanInProgress_) { | |
2573 console.error('Scan-completed event recieved. But scan is not started.'); | |
2574 return; | |
2575 } | |
2576 | |
2577 if (this.commandHandler) | |
2578 this.commandHandler.updateAvailability(); | |
2579 this.hideSpinnerLater_(); | |
2580 this.refreshCurrentDirectoryMetadata_(); | |
2581 | |
2582 if (this.scanUpdatedTimer_) { | |
2583 clearTimeout(this.scanUpdatedTimer_); | |
2584 this.scanUpdatedTimer_ = null; | |
2585 } | |
2586 | |
2587 // To avoid flickering postpone updating the ui by a small amount of time. | |
2588 // There is a high chance, that metadata will be received within 50 ms. | |
2589 this.scanCompletedTimer_ = setTimeout(function() { | |
2590 // Check if batch updates are already finished by onScanUpdated_(). | |
2591 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { | |
2592 this.scanUpdatedAtLeastOnceOrCompleted_ = true; | |
2593 this.updateMiddleBarVisibility_(); | |
2594 } | |
2595 | |
2596 this.scanInProgress_ = false; | |
2597 this.table_.list.endBatchUpdates(); | |
2598 this.grid_.endBatchUpdates(); | |
2599 this.scanCompletedTimer_ = null; | |
2600 }.bind(this), 50); | |
2601 }; | |
2602 | |
2603 /** | |
2604 * @private | |
2605 */ | |
2606 FileManager.prototype.onScanUpdated_ = function() { | |
2607 if (!this.scanInProgress_) { | |
2608 console.error('Scan-updated event recieved. But scan is not started.'); | |
2609 return; | |
2610 } | |
2611 | |
2612 if (this.scanUpdatedTimer_ || this.scanCompletedTimer_) | |
2613 return; | |
2614 | |
2615 // Show contents incrementally by finishing batch updated, but only after | |
2616 // 200ms elapsed, to avoid flickering when it is not necessary. | |
2617 this.scanUpdatedTimer_ = setTimeout(function() { | |
2618 // We need to hide the spinner only once. | |
2619 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { | |
2620 this.scanUpdatedAtLeastOnceOrCompleted_ = true; | |
2621 this.hideSpinnerLater_(); | |
2622 this.updateMiddleBarVisibility_(); | |
2623 } | |
2624 | |
2625 // Update the UI. | |
2626 if (this.scanInProgress_) { | |
2627 this.table_.list.endBatchUpdates(); | |
2628 this.grid_.endBatchUpdates(); | |
2629 this.table_.list.startBatchUpdates(); | |
2630 this.grid_.startBatchUpdates(); | |
2631 } | |
2632 this.scanUpdatedTimer_ = null; | |
2633 }.bind(this), 200); | |
2634 }; | |
2635 | |
2636 /** | |
2637 * @private | |
2638 */ | |
2639 FileManager.prototype.onScanCancelled_ = function() { | |
2640 if (!this.scanInProgress_) { | |
2641 console.error('Scan-cancelled event recieved. But scan is not started.'); | |
2642 return; | |
2643 } | |
2644 | |
2645 if (this.commandHandler) | |
2646 this.commandHandler.updateAvailability(); | |
2647 this.hideSpinnerLater_(); | |
2648 if (this.scanCompletedTimer_) { | |
2649 clearTimeout(this.scanCompletedTimer_); | |
2650 this.scanCompletedTimer_ = null; | |
2651 } | |
2652 if (this.scanUpdatedTimer_) { | |
2653 clearTimeout(this.scanUpdatedTimer_); | |
2654 this.scanUpdatedTimer_ = null; | |
2655 } | |
2656 // Finish unfinished batch updates. | |
2657 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { | |
2658 this.scanUpdatedAtLeastOnceOrCompleted_ = true; | |
2659 this.updateMiddleBarVisibility_(); | |
2660 } | |
2661 | |
2662 this.scanInProgress_ = false; | |
2663 this.table_.list.endBatchUpdates(); | |
2664 this.grid_.endBatchUpdates(); | |
2665 }; | |
2666 | |
2667 /** | |
2668 * Handle the 'rescan-completed' from the DirectoryModel. | |
2669 * @private | |
2670 */ | |
2671 FileManager.prototype.onRescanCompleted_ = function() { | |
2672 this.refreshCurrentDirectoryMetadata_(); | |
2673 this.selectionHandler_.onFileSelectionChanged(); | |
2674 }; | |
2675 | |
2676 /** | |
2677 * @private | |
2678 */ | |
2679 FileManager.prototype.cancelSpinnerTimeout_ = function() { | |
2680 if (this.showSpinnerTimeout_) { | |
2681 clearTimeout(this.showSpinnerTimeout_); | |
2682 this.showSpinnerTimeout_ = null; | |
2683 } | |
2684 }; | |
2685 | |
2686 /** | |
2687 * @private | |
2688 */ | |
2689 FileManager.prototype.hideSpinnerLater_ = function() { | |
2690 this.cancelSpinnerTimeout_(); | |
2691 this.showSpinner_(false); | |
2692 }; | |
2693 | |
2694 /** | |
2695 * @param {boolean} on True to show, false to hide. | |
2696 * @private | |
2697 */ | |
2698 FileManager.prototype.showSpinner_ = function(on) { | |
2699 if (on && this.directoryModel_ && this.directoryModel_.isScanning()) | |
2700 this.spinner_.hidden = false; | |
2701 | |
2702 if (!on && (!this.directoryModel_ || | |
2703 !this.directoryModel_.isScanning() || | |
2704 this.directoryModel_.getFileList().length != 0)) { | |
2705 this.spinner_.hidden = true; | |
2706 } | |
2707 }; | |
2708 | |
2709 FileManager.prototype.createNewFolder = function() { | |
2710 var defaultName = str('DEFAULT_NEW_FOLDER_NAME'); | |
2711 | |
2712 // Find a name that doesn't exist in the data model. | |
2713 var files = this.directoryModel_.getFileList(); | |
2714 var hash = {}; | |
2715 for (var i = 0; i < files.length; i++) { | |
2716 var name = files.item(i).name; | |
2717 // Filtering names prevents from conflicts with prototype's names | |
2718 // and '__proto__'. | |
2719 if (name.substring(0, defaultName.length) == defaultName) | |
2720 hash[name] = 1; | |
2721 } | |
2722 | |
2723 var baseName = defaultName; | |
2724 var separator = ''; | |
2725 var suffix = ''; | |
2726 var index = ''; | |
2727 | |
2728 var advance = function() { | |
2729 separator = ' ('; | |
2730 suffix = ')'; | |
2731 index++; | |
2732 }; | |
2733 | |
2734 var current = function() { | |
2735 return baseName + separator + index + suffix; | |
2736 }; | |
2737 | |
2738 // Accessing hasOwnProperty is safe since hash properties filtered. | |
2739 while (hash.hasOwnProperty(current())) { | |
2740 advance(); | |
2741 } | |
2742 | |
2743 var self = this; | |
2744 var list = self.currentList_; | |
2745 var tryCreate = function() { | |
2746 self.directoryModel_.createDirectory(current(), | |
2747 onSuccess, onError); | |
2748 }; | |
2749 | |
2750 var onSuccess = function(entry) { | |
2751 metrics.recordUserAction('CreateNewFolder'); | |
2752 list.selectedItem = entry; | |
2753 self.initiateRename(); | |
2754 }; | |
2755 | |
2756 var onError = function(error) { | |
2757 self.alert.show(strf('ERROR_CREATING_FOLDER', current(), | |
2758 util.getFileErrorString(error.code))); | |
2759 }; | |
2760 | |
2761 tryCreate(); | |
2762 }; | |
2763 | |
2764 /** | |
2765 * @param {Event} event Click event. | |
2766 * @private | |
2767 */ | |
2768 FileManager.prototype.onDetailViewButtonClick_ = function(event) { | |
2769 this.setListType(FileManager.ListType.DETAIL); | |
2770 this.currentList_.focus(); | |
2771 }; | |
2772 | |
2773 /** | |
2774 * @param {Event} event Click event. | |
2775 * @private | |
2776 */ | |
2777 FileManager.prototype.onThumbnailViewButtonClick_ = function(event) { | |
2778 this.setListType(FileManager.ListType.THUMBNAIL); | |
2779 this.currentList_.focus(); | |
2780 }; | |
2781 | |
2782 /** | |
2783 * KeyDown event handler for the document. | |
2784 * @param {Event} event Key event. | |
2785 * @private | |
2786 */ | |
2787 FileManager.prototype.onKeyDown_ = function(event) { | |
2788 if (event.srcElement === this.renameInput_) { | |
2789 // Ignore keydown handler in the rename input box. | |
2790 return; | |
2791 } | |
2792 | |
2793 switch (util.getKeyModifiers(event) + event.keyCode) { | |
2794 case 'Ctrl-190': // Ctrl-. => Toggle filter files. | |
2795 this.fileFilter_.setFilterHidden( | |
2796 !this.fileFilter_.isFilterHiddenOn()); | |
2797 event.preventDefault(); | |
2798 return; | |
2799 | |
2800 case '27': // Escape => Cancel dialog. | |
2801 if (this.fileOperationManager_ && | |
2802 this.fileOperationManager_.isRunning()) { | |
2803 // If there is a copy in progress, ESC will cancel it. | |
2804 event.preventDefault(); | |
2805 this.fileOperationManager_.requestCancel(); | |
2806 return; | |
2807 } | |
2808 | |
2809 if (this.dialogType != DialogType.FULL_PAGE) { | |
2810 // If there is nothing else for ESC to do, then cancel the dialog. | |
2811 event.preventDefault(); | |
2812 this.cancelButton_.click(); | |
2813 } | |
2814 break; | |
2815 } | |
2816 }; | |
2817 | |
2818 /** | |
2819 * KeyDown event handler for the div#list-container element. | |
2820 * @param {Event} event Key event. | |
2821 * @private | |
2822 */ | |
2823 FileManager.prototype.onListKeyDown_ = function(event) { | |
2824 if (event.srcElement.tagName == 'INPUT') { | |
2825 // Ignore keydown handler in the rename input box. | |
2826 return; | |
2827 } | |
2828 | |
2829 switch (util.getKeyModifiers(event) + event.keyCode) { | |
2830 case '8': // Backspace => Up one directory. | |
2831 event.preventDefault(); | |
2832 var path = this.getCurrentDirectory(); | |
2833 if (path && !PathUtil.isRootPath(path)) { | |
2834 var path = path.replace(/\/[^\/]+$/, ''); | |
2835 this.directoryModel_.changeDirectory(path); | |
2836 } | |
2837 break; | |
2838 | |
2839 case '13': // Enter => Change directory or perform default action. | |
2840 // TODO(dgozman): move directory action to dispatchSelectionAction. | |
2841 var selection = this.getSelection(); | |
2842 if (selection.totalCount == 1 && | |
2843 selection.entries[0].isDirectory && | |
2844 this.dialogType != DialogType.SELECT_FOLDER && | |
2845 this.dialogType != DialogType.SELECT_UPLOAD_FOLDER) { | |
2846 event.preventDefault(); | |
2847 this.onDirectoryAction_(selection.entries[0]); | |
2848 } else if (this.dispatchSelectionAction_()) { | |
2849 event.preventDefault(); | |
2850 } | |
2851 break; | |
2852 } | |
2853 | |
2854 switch (event.keyIdentifier) { | |
2855 case 'Home': | |
2856 case 'End': | |
2857 case 'Up': | |
2858 case 'Down': | |
2859 case 'Left': | |
2860 case 'Right': | |
2861 // When navigating with keyboard we hide the distracting mouse hover | |
2862 // highlighting until the user moves the mouse again. | |
2863 this.setNoHover_(true); | |
2864 break; | |
2865 } | |
2866 }; | |
2867 | |
2868 /** | |
2869 * Suppress/restore hover highlighting in the list container. | |
2870 * @param {boolean} on True to temporarity hide hover state. | |
2871 * @private | |
2872 */ | |
2873 FileManager.prototype.setNoHover_ = function(on) { | |
2874 if (on) { | |
2875 this.listContainer_.classList.add('nohover'); | |
2876 } else { | |
2877 this.listContainer_.classList.remove('nohover'); | |
2878 } | |
2879 }; | |
2880 | |
2881 /** | |
2882 * KeyPress event handler for the div#list-container element. | |
2883 * @param {Event} event Key event. | |
2884 * @private | |
2885 */ | |
2886 FileManager.prototype.onListKeyPress_ = function(event) { | |
2887 if (event.srcElement.tagName == 'INPUT') { | |
2888 // Ignore keypress handler in the rename input box. | |
2889 return; | |
2890 } | |
2891 | |
2892 if (event.ctrlKey || event.metaKey || event.altKey) | |
2893 return; | |
2894 | |
2895 var now = new Date(); | |
2896 var char = String.fromCharCode(event.charCode).toLowerCase(); | |
2897 var text = now - this.textSearchState_.date > 1000 ? '' : | |
2898 this.textSearchState_.text; | |
2899 this.textSearchState_ = {text: text + char, date: now}; | |
2900 | |
2901 this.doTextSearch_(); | |
2902 }; | |
2903 | |
2904 /** | |
2905 * Mousemove event handler for the div#list-container element. | |
2906 * @param {Event} event Mouse event. | |
2907 * @private | |
2908 */ | |
2909 FileManager.prototype.onListMouseMove_ = function(event) { | |
2910 // The user grabbed the mouse, restore the hover highlighting. | |
2911 this.setNoHover_(false); | |
2912 }; | |
2913 | |
2914 /** | |
2915 * Performs a 'text search' - selects a first list entry with name | |
2916 * starting with entered text (case-insensitive). | |
2917 * @private | |
2918 */ | |
2919 FileManager.prototype.doTextSearch_ = function() { | |
2920 var text = this.textSearchState_.text; | |
2921 if (!text) | |
2922 return; | |
2923 | |
2924 var dm = this.directoryModel_.getFileList(); | |
2925 for (var index = 0; index < dm.length; ++index) { | |
2926 var name = dm.item(index).name; | |
2927 if (name.substring(0, text.length).toLowerCase() == text) { | |
2928 this.currentList_.selectionModel.selectedIndexes = [index]; | |
2929 return; | |
2930 } | |
2931 } | |
2932 | |
2933 this.textSearchState_.text = ''; | |
2934 }; | |
2935 | |
2936 /** | |
2937 * Handle a click of the cancel button. Closes the window. | |
2938 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 | |
2939 * | |
2940 * @param {Event} event The click event. | |
2941 * @private | |
2942 */ | |
2943 FileManager.prototype.onCancel_ = function(event) { | |
2944 chrome.fileBrowserPrivate.cancelDialog(); | |
2945 this.onUnload_(); | |
2946 window.close(); | |
2947 }; | |
2948 | |
2949 /** | |
2950 * Resolves selected file urls returned from an Open dialog. | |
2951 * | |
2952 * For drive files this involves some special treatment. | |
2953 * Starts getting drive files if needed. | |
2954 * | |
2955 * @param {Array.<string>} fileUrls Drive URLs. | |
2956 * @param {function(Array.<string>)} callback To be called with fixed URLs. | |
2957 * @private | |
2958 */ | |
2959 FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) { | |
2960 if (this.isOnDrive()) { | |
2961 chrome.fileBrowserPrivate.getDriveFiles( | |
2962 fileUrls, | |
2963 function(localPaths) { | |
2964 callback(fileUrls); | |
2965 }); | |
2966 } else { | |
2967 callback(fileUrls); | |
2968 } | |
2969 }; | |
2970 | |
2971 /** | |
2972 * Closes this modal dialog with some files selected. | |
2973 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 | |
2974 * @param {Object} selection Contains urls, filterIndex and multiple fields. | |
2975 * @private | |
2976 */ | |
2977 FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) { | |
2978 var self = this; | |
2979 function callback() { | |
2980 self.onUnload_(); | |
2981 window.close(); | |
2982 } | |
2983 if (selection.multiple) { | |
2984 chrome.fileBrowserPrivate.selectFiles( | |
2985 selection.urls, this.params_.shouldReturnLocalPath, callback); | |
2986 } else { | |
2987 var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE); | |
2988 chrome.fileBrowserPrivate.selectFile( | |
2989 selection.urls[0], selection.filterIndex, forOpening, | |
2990 this.params_.shouldReturnLocalPath, callback); | |
2991 } | |
2992 }; | |
2993 | |
2994 /** | |
2995 * Tries to close this modal dialog with some files selected. | |
2996 * Performs preprocessing if needed (e.g. for Drive). | |
2997 * @param {Object} selection Contains urls, filterIndex and multiple fields. | |
2998 * @private | |
2999 */ | |
3000 FileManager.prototype.selectFilesAndClose_ = function(selection) { | |
3001 if (!this.isOnDrive() || | |
3002 this.dialogType == DialogType.SELECT_SAVEAS_FILE) { | |
3003 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); | |
3004 return; | |
3005 } | |
3006 | |
3007 var shade = this.document_.createElement('div'); | |
3008 shade.className = 'shade'; | |
3009 var footer = this.dialogDom_.querySelector('.button-panel'); | |
3010 var progress = footer.querySelector('.progress-track'); | |
3011 progress.style.width = '0%'; | |
3012 var cancelled = false; | |
3013 | |
3014 var progressMap = {}; | |
3015 var filesStarted = 0; | |
3016 var filesTotal = selection.urls.length; | |
3017 for (var index = 0; index < selection.urls.length; index++) { | |
3018 progressMap[selection.urls[index]] = -1; | |
3019 } | |
3020 var lastPercent = 0; | |
3021 var bytesTotal = 0; | |
3022 var bytesDone = 0; | |
3023 | |
3024 var onFileTransfersUpdated = function(statusList) { | |
3025 for (var index = 0; index < statusList.length; index++) { | |
3026 var status = statusList[index]; | |
3027 var escaped = encodeURI(status.fileUrl); | |
3028 if (!(escaped in progressMap)) continue; | |
3029 if (status.total == -1) continue; | |
3030 | |
3031 var old = progressMap[escaped]; | |
3032 if (old == -1) { | |
3033 // -1 means we don't know file size yet. | |
3034 bytesTotal += status.total; | |
3035 filesStarted++; | |
3036 old = 0; | |
3037 } | |
3038 bytesDone += status.processed - old; | |
3039 progressMap[escaped] = status.processed; | |
3040 } | |
3041 | |
3042 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal; | |
3043 // For files we don't have information about, assume the progress is zero. | |
3044 percent = percent * filesStarted / filesTotal * 100; | |
3045 // Do not decrease the progress. This may happen, if first downloaded | |
3046 // file is small, and the second one is large. | |
3047 lastPercent = Math.max(lastPercent, percent); | |
3048 progress.style.width = lastPercent + '%'; | |
3049 }.bind(this); | |
3050 | |
3051 var setup = function() { | |
3052 this.document_.querySelector('.dialog-container').appendChild(shade); | |
3053 setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100); | |
3054 footer.setAttribute('progress', 'progress'); | |
3055 this.cancelButton_.removeEventListener('click', this.onCancelBound_); | |
3056 this.cancelButton_.addEventListener('click', onCancel); | |
3057 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener( | |
3058 onFileTransfersUpdated); | |
3059 }.bind(this); | |
3060 | |
3061 var cleanup = function() { | |
3062 shade.parentNode.removeChild(shade); | |
3063 footer.removeAttribute('progress'); | |
3064 this.cancelButton_.removeEventListener('click', onCancel); | |
3065 this.cancelButton_.addEventListener('click', this.onCancelBound_); | |
3066 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( | |
3067 onFileTransfersUpdated); | |
3068 }.bind(this); | |
3069 | |
3070 var onCancel = function() { | |
3071 cancelled = true; | |
3072 // According to API cancel may fail, but there is no proper UI to reflect | |
3073 // this. So, we just silently assume that everything is cancelled. | |
3074 chrome.fileBrowserPrivate.cancelFileTransfers( | |
3075 selection.urls, function(response) {}); | |
3076 cleanup(); | |
3077 }.bind(this); | |
3078 | |
3079 var onResolved = function(resolvedUrls) { | |
3080 if (cancelled) return; | |
3081 cleanup(); | |
3082 selection.urls = resolvedUrls; | |
3083 // Call next method on a timeout, as it's unsafe to | |
3084 // close a window from a callback. | |
3085 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); | |
3086 }.bind(this); | |
3087 | |
3088 var onProperties = function(properties) { | |
3089 for (var i = 0; i < properties.length; i++) { | |
3090 if (!properties[i] || properties[i].present) { | |
3091 // For files already in GCache, we don't get any transfer updates. | |
3092 filesTotal--; | |
3093 } | |
3094 } | |
3095 this.resolveSelectResults_(selection.urls, onResolved); | |
3096 }.bind(this); | |
3097 | |
3098 setup(); | |
3099 this.metadataCache_.get(selection.urls, 'drive', onProperties); | |
3100 }; | |
3101 | |
3102 /** | |
3103 * Handle a click of the ok button. | |
3104 * | |
3105 * The ok button has different UI labels depending on the type of dialog, but | |
3106 * in code it's always referred to as 'ok'. | |
3107 * | |
3108 * @param {Event} event The click event. | |
3109 * @private | |
3110 */ | |
3111 FileManager.prototype.onOk_ = function(event) { | |
3112 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) { | |
3113 // Save-as doesn't require a valid selection from the list, since | |
3114 // we're going to take the filename from the text input. | |
3115 var filename = this.filenameInput_.value; | |
3116 if (!filename) | |
3117 throw new Error('Missing filename!'); | |
3118 | |
3119 var directory = this.getCurrentDirectoryEntry(); | |
3120 var currentDirUrl = directory.toURL(); | |
3121 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/') | |
3122 currentDirUrl += '/'; | |
3123 this.validateFileName_(currentDirUrl, filename, function(isValid) { | |
3124 if (!isValid) | |
3125 return; | |
3126 | |
3127 if (util.isFakeDirectoryEntry(directory)) { | |
3128 // Can't save a file into a fake directory. | |
3129 return; | |
3130 } | |
3131 | |
3132 var selectFileAndClose = function() { | |
3133 this.selectFilesAndClose_({ | |
3134 urls: [currentDirUrl + encodeURIComponent(filename)], | |
3135 multiple: false, | |
3136 filterIndex: this.getSelectedFilterIndex_(filename) | |
3137 }); | |
3138 }.bind(this); | |
3139 | |
3140 directory.getFile( | |
3141 filename, {create: false}, | |
3142 function(entry) { | |
3143 // An existing file is found. Show confirmation dialog to | |
3144 // overwrite it. If the user select "OK" on the dialog, save it. | |
3145 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename), | |
3146 selectFileAndClose); | |
3147 }.bind(this), | |
3148 function(error) { | |
3149 if (error.code == FileError.NOT_FOUND_ERR) { | |
3150 // The file does not exist, so it should be ok to create a | |
3151 // new file. | |
3152 selectFileAndClose(); | |
3153 return; | |
3154 } | |
3155 if (error.code == FileError.TYPE_MISMATCH_ERR) { | |
3156 // An directory is found. | |
3157 // Do not allow to overwrite directory. | |
3158 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename)); | |
3159 return; | |
3160 } | |
3161 | |
3162 // Unexpected error. | |
3163 console.error('File save failed: ' + error.code); | |
3164 }.bind(this)); | |
3165 }.bind(this)); | |
3166 return; | |
3167 } | |
3168 | |
3169 var files = []; | |
3170 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes; | |
3171 | |
3172 if ((this.dialogType == DialogType.SELECT_FOLDER || | |
3173 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER) && | |
3174 selectedIndexes.length == 0) { | |
3175 var url = this.getCurrentDirectoryURL(); | |
3176 var singleSelection = { | |
3177 urls: [url], | |
3178 multiple: false, | |
3179 filterIndex: this.getSelectedFilterIndex_() | |
3180 }; | |
3181 this.selectFilesAndClose_(singleSelection); | |
3182 return; | |
3183 } | |
3184 | |
3185 // All other dialog types require at least one selected list item. | |
3186 // The logic to control whether or not the ok button is enabled should | |
3187 // prevent us from ever getting here, but we sanity check to be sure. | |
3188 if (!selectedIndexes.length) | |
3189 throw new Error('Nothing selected!'); | |
3190 | |
3191 var dm = this.directoryModel_.getFileList(); | |
3192 for (var i = 0; i < selectedIndexes.length; i++) { | |
3193 var entry = dm.item(selectedIndexes[i]); | |
3194 if (!entry) { | |
3195 console.error('Error locating selected file at index: ' + i); | |
3196 continue; | |
3197 } | |
3198 | |
3199 files.push(entry.toURL()); | |
3200 } | |
3201 | |
3202 // Multi-file selection has no other restrictions. | |
3203 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) { | |
3204 var multipleSelection = { | |
3205 urls: files, | |
3206 multiple: true | |
3207 }; | |
3208 this.selectFilesAndClose_(multipleSelection); | |
3209 return; | |
3210 } | |
3211 | |
3212 // Everything else must have exactly one. | |
3213 if (files.length > 1) | |
3214 throw new Error('Too many files selected!'); | |
3215 | |
3216 var selectedEntry = dm.item(selectedIndexes[0]); | |
3217 | |
3218 if (this.dialogType == DialogType.SELECT_FOLDER || | |
3219 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER) { | |
3220 if (!selectedEntry.isDirectory) | |
3221 throw new Error('Selected entry is not a folder!'); | |
3222 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) { | |
3223 if (!selectedEntry.isFile) | |
3224 throw new Error('Selected entry is not a file!'); | |
3225 } | |
3226 | |
3227 var singleSelection = { | |
3228 urls: [files[0]], | |
3229 multiple: false, | |
3230 filterIndex: this.getSelectedFilterIndex_() | |
3231 }; | |
3232 this.selectFilesAndClose_(singleSelection); | |
3233 }; | |
3234 | |
3235 /** | |
3236 * Verifies the user entered name for file or folder to be created or | |
3237 * renamed to. Name restrictions must correspond to File API restrictions | |
3238 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is | |
3239 * out of date (spec is | |
3240 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to | |
3241 * be fixed. Shows message box if the name is invalid. | |
3242 * | |
3243 * It also verifies if the name length is in the limit of the filesystem. | |
3244 * | |
3245 * @param {string} parentUrl The URL of the parent directory entry. | |
3246 * @param {string} name New file or folder name. | |
3247 * @param {function} onDone Function to invoke when user closes the | |
3248 * warning box or immediatelly if file name is correct. If the name was | |
3249 * valid it is passed true, and false otherwise. | |
3250 * @private | |
3251 */ | |
3252 FileManager.prototype.validateFileName_ = function(parentUrl, name, onDone) { | |
3253 var msg; | |
3254 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name); | |
3255 if (testResult) { | |
3256 msg = strf('ERROR_INVALID_CHARACTER', testResult[0]); | |
3257 } else if (/^\s*$/i.test(name)) { | |
3258 msg = str('ERROR_WHITESPACE_NAME'); | |
3259 } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) { | |
3260 msg = str('ERROR_RESERVED_NAME'); | |
3261 } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') { | |
3262 msg = str('ERROR_HIDDEN_NAME'); | |
3263 } | |
3264 | |
3265 if (msg) { | |
3266 this.alert.show(msg, function() { | |
3267 onDone(false); | |
3268 }); | |
3269 return; | |
3270 } | |
3271 | |
3272 var self = this; | |
3273 chrome.fileBrowserPrivate.validatePathNameLength( | |
3274 parentUrl, name, function(valid) { | |
3275 if (!valid) { | |
3276 self.alert.show(str('ERROR_LONG_NAME'), | |
3277 function() { onDone(false); }); | |
3278 } else { | |
3279 onDone(true); | |
3280 } | |
3281 }); | |
3282 }; | |
3283 | |
3284 /** | |
3285 * Handler invoked on preference setting in drive context menu. | |
3286 * | |
3287 * @param {string} pref The preference to alter. | |
3288 * @param {boolean} inverted Invert the value if true. | |
3289 * @param {Event} event The click event. | |
3290 * @private | |
3291 */ | |
3292 FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) { | |
3293 var newValue = !event.target.hasAttribute('checked'); | |
3294 if (newValue) | |
3295 event.target.setAttribute('checked', 'checked'); | |
3296 else | |
3297 event.target.removeAttribute('checked'); | |
3298 | |
3299 var changeInfo = {}; | |
3300 changeInfo[pref] = inverted ? !newValue : newValue; | |
3301 chrome.fileBrowserPrivate.setPreferences(changeInfo); | |
3302 }; | |
3303 | |
3304 /** | |
3305 * Invoked when the search box is changed. | |
3306 * | |
3307 * @param {Event} event The changed event. | |
3308 * @private | |
3309 */ | |
3310 FileManager.prototype.onSearchBoxUpdate_ = function(event) { | |
3311 var searchString = this.searchBox_.value; | |
3312 | |
3313 if (this.isOnDrive()) { | |
3314 // When the search text is changed, finishes the search and showes back | |
3315 // the last directory by passing an empty string to | |
3316 // {@code DirectoryModel.search()}. | |
3317 if (this.directoryModel_.isSearching() && | |
3318 this.lastSearchQuery_ != searchString) { | |
3319 this.doSearch(''); | |
3320 } | |
3321 | |
3322 // On drive, incremental search is not invoked since we have an auto- | |
3323 // complete suggestion instead. | |
3324 return; | |
3325 } | |
3326 | |
3327 this.search_(searchString); | |
3328 }; | |
3329 | |
3330 /** | |
3331 * Handle the search clear button click. | |
3332 * @private | |
3333 */ | |
3334 FileManager.prototype.onSearchClearButtonClick_ = function() { | |
3335 this.ui_.searchBox.clear(); | |
3336 this.onSearchBoxUpdate_(); | |
3337 }; | |
3338 | |
3339 /** | |
3340 * Search files and update the list with the search result. | |
3341 * | |
3342 * @param {string} searchString String to be searched with. | |
3343 * @private | |
3344 */ | |
3345 FileManager.prototype.search_ = function(searchString) { | |
3346 var noResultsDiv = this.document_.getElementById('no-search-results'); | |
3347 | |
3348 var reportEmptySearchResults = function() { | |
3349 if (this.directoryModel_.getFileList().length === 0) { | |
3350 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags, | |
3351 // hence we escapes |searchString| here. | |
3352 var html = strf('SEARCH_NO_MATCHING_FILES_HTML', | |
3353 util.htmlEscape(searchString)); | |
3354 noResultsDiv.innerHTML = html; | |
3355 noResultsDiv.setAttribute('show', 'true'); | |
3356 } else { | |
3357 noResultsDiv.removeAttribute('show'); | |
3358 } | |
3359 }; | |
3360 | |
3361 var hideNoResultsDiv = function() { | |
3362 noResultsDiv.removeAttribute('show'); | |
3363 }; | |
3364 | |
3365 this.doSearch(searchString, | |
3366 reportEmptySearchResults.bind(this), | |
3367 hideNoResultsDiv.bind(this)); | |
3368 }; | |
3369 | |
3370 /** | |
3371 * Performs search and displays results. | |
3372 * | |
3373 * @param {string} query Query that will be searched for. | |
3374 * @param {function()=} opt_onSearchRescan Function that will be called when | |
3375 * the search directory is rescanned (i.e. search results are displayed). | |
3376 * @param {function()=} opt_onClearSearch Function to be called when search | |
3377 * state gets cleared. | |
3378 */ | |
3379 FileManager.prototype.doSearch = function( | |
3380 searchString, opt_onSearchRescan, opt_onClearSearch) { | |
3381 var onSearchRescan = opt_onSearchRescan || function() {}; | |
3382 var onClearSearch = opt_onClearSearch || function() {}; | |
3383 | |
3384 this.lastSearchQuery_ = searchString; | |
3385 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch); | |
3386 }; | |
3387 | |
3388 /** | |
3389 * Requests autocomplete suggestions for files on Drive. | |
3390 * Once the suggestions are returned, the autocomplete popup will show up. | |
3391 * | |
3392 * @param {string} query The text to autocomplete from. | |
3393 * @private | |
3394 */ | |
3395 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) { | |
3396 query = query.trimLeft(); | |
3397 | |
3398 // Only Drive supports auto-compelete | |
3399 if (!this.isOnDrive()) | |
3400 return; | |
3401 | |
3402 // Remember the most recent query. If there is an other request in progress, | |
3403 // then it's result will be discarded and it will call a new request for | |
3404 // this query. | |
3405 this.lastAutocompleteQuery_ = query; | |
3406 if (this.autocompleteSuggestionsBusy_) | |
3407 return; | |
3408 | |
3409 // The autocomplete list should be resized and repositioned here as the | |
3410 // search box is resized when it's focused. | |
3411 this.autocompleteList_.syncWidthAndPositionToInput(); | |
3412 | |
3413 if (!query) { | |
3414 this.autocompleteList_.suggestions = []; | |
3415 return; | |
3416 } | |
3417 | |
3418 var headerItem = {isHeaderItem: true, searchQuery: query}; | |
3419 if (!this.autocompleteList_.dataModel || | |
3420 this.autocompleteList_.dataModel.length == 0) | |
3421 this.autocompleteList_.suggestions = [headerItem]; | |
3422 else | |
3423 // Updates only the head item to prevent a flickering on typing. | |
3424 this.autocompleteList_.dataModel.splice(0, 1, headerItem); | |
3425 | |
3426 this.autocompleteSuggestionsBusy_ = true; | |
3427 | |
3428 var searchParams = { | |
3429 'query': query, | |
3430 'types': 'ALL', | |
3431 'maxResults': 4 | |
3432 }; | |
3433 chrome.fileBrowserPrivate.searchDriveMetadata( | |
3434 searchParams, | |
3435 function(suggestions) { | |
3436 this.autocompleteSuggestionsBusy_ = false; | |
3437 | |
3438 // Discard results for previous requests and fire a new search | |
3439 // for the most recent query. | |
3440 if (query != this.lastAutocompleteQuery_) { | |
3441 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_); | |
3442 return; | |
3443 } | |
3444 | |
3445 // Keeps the items in the suggestion list. | |
3446 this.autocompleteList_.suggestions = [headerItem].concat(suggestions); | |
3447 }.bind(this)); | |
3448 }; | |
3449 | |
3450 /** | |
3451 * Opens the currently selected suggestion item. | |
3452 * @private | |
3453 */ | |
3454 FileManager.prototype.openAutocompleteSuggestion_ = function() { | |
3455 var selectedItem = this.autocompleteList_.selectedItem; | |
3456 | |
3457 // If the entry is the search item or no entry is selected, just change to | |
3458 // the search result. | |
3459 if (!selectedItem || selectedItem.isHeaderItem) { | |
3460 var query = selectedItem ? | |
3461 selectedItem.searchQuery : this.searchBox_.value; | |
3462 this.search_(query); | |
3463 return; | |
3464 } | |
3465 | |
3466 var entry = selectedItem.entry; | |
3467 // If the entry is a directory, just change the directory. | |
3468 if (entry.isDirectory) { | |
3469 this.onDirectoryAction_(entry); | |
3470 return; | |
3471 } | |
3472 | |
3473 var urls = [entry.toURL()]; | |
3474 var self = this; | |
3475 | |
3476 // To open a file, first get the mime type. | |
3477 this.metadataCache_.get(urls, 'drive', function(props) { | |
3478 var mimeType = props[0].contentMimeType || ''; | |
3479 var mimeTypes = [mimeType]; | |
3480 var openIt = function() { | |
3481 if (self.dialogType == DialogType.FULL_PAGE) { | |
3482 var tasks = new FileTasks(self); | |
3483 tasks.init(urls, mimeTypes); | |
3484 tasks.executeDefault(); | |
3485 } else { | |
3486 self.onOk_(); | |
3487 } | |
3488 }; | |
3489 | |
3490 // Change the current directory to the directory that contains the | |
3491 // selected file. Note that this is necessary for an image or a video, | |
3492 // which should be opened in the gallery mode, as the gallery mode | |
3493 // requires the entry to be in the current directory model. For | |
3494 // consistency, the current directory is always changed regardless of | |
3495 // the file type. | |
3496 entry.getParent(function(parent) { | |
3497 var onDirectoryChanged = function(event) { | |
3498 self.directoryModel_.removeEventListener('scan-completed', | |
3499 onDirectoryChanged); | |
3500 self.directoryModel_.selectEntry(entry.name); | |
3501 openIt(); | |
3502 }; | |
3503 // changeDirectory() returns immediately. We should wait until the | |
3504 // directory scan is complete. | |
3505 self.directoryModel_.addEventListener('scan-completed', | |
3506 onDirectoryChanged); | |
3507 self.directoryModel_.changeDirectory( | |
3508 parent.fullPath, | |
3509 function() { | |
3510 // Remove the listner if the change directory failed. | |
3511 self.directoryModel_.removeEventListener('scan-completed', | |
3512 onDirectoryChanged); | |
3513 }); | |
3514 }); | |
3515 }); | |
3516 }; | |
3517 | |
3518 /** | |
3519 * Opens the default app change dialog. | |
3520 */ | |
3521 FileManager.prototype.showChangeDefaultAppPicker = function() { | |
3522 var onActionsReady = function(actions, rememberedActionId) { | |
3523 var items = []; | |
3524 var defaultIndex = -1; | |
3525 for (var i = 0; i < actions.length; i++) { | |
3526 if (actions[i].hidden) | |
3527 continue; | |
3528 var title = actions[i].title; | |
3529 if (actions[i].id == rememberedActionId) { | |
3530 title += ' ' + loadTimeData.getString('DEFAULT_ACTION_LABEL'); | |
3531 defaultIndex = i; | |
3532 } | |
3533 var item = { | |
3534 id: actions[i].id, | |
3535 label: title, | |
3536 class: actions[i].class, | |
3537 iconUrl: actions[i].icon100 | |
3538 }; | |
3539 items.push(item); | |
3540 } | |
3541 var show = this.defaultTaskPicker.showOkCancelDialog( | |
3542 str('CHANGE_DEFAULT_APP_BUTTON_LABEL'), | |
3543 '', | |
3544 items, | |
3545 defaultIndex, | |
3546 function(action) { | |
3547 ActionChoiceUtil.setRememberedActionId(action.id); | |
3548 }); | |
3549 if (!show) | |
3550 console.error('DefaultTaskPicker can\'t be shown.'); | |
3551 }.bind(this); | |
3552 | |
3553 ActionChoiceUtil.getDefinedActions(loadTimeData, function(actions) { | |
3554 ActionChoiceUtil.getRememberedActionId(function(actionId) { | |
3555 onActionsReady(actions, actionId); | |
3556 }); | |
3557 }); | |
3558 }; | |
3559 | |
3560 FileManager.prototype.decorateSplitter = function(splitterElement) { | |
3561 var self = this; | |
3562 | |
3563 var Splitter = cr.ui.Splitter; | |
3564 | |
3565 var customSplitter = cr.ui.define('div'); | |
3566 | |
3567 customSplitter.prototype = { | |
3568 __proto__: Splitter.prototype, | |
3569 | |
3570 handleSplitterDragStart: function(e) { | |
3571 Splitter.prototype.handleSplitterDragStart.apply(this, arguments); | |
3572 this.ownerDocument.documentElement.classList.add('col-resize'); | |
3573 }, | |
3574 | |
3575 handleSplitterDragMove: function(deltaX) { | |
3576 Splitter.prototype.handleSplitterDragMove.apply(this, arguments); | |
3577 self.onResize_(); | |
3578 }, | |
3579 | |
3580 handleSplitterDragEnd: function(e) { | |
3581 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments); | |
3582 this.ownerDocument.documentElement.classList.remove('col-resize'); | |
3583 } | |
3584 }; | |
3585 | |
3586 customSplitter.decorate(splitterElement); | |
3587 }; | |
3588 | |
3589 /** | |
3590 * Updates default action menu item to match passed taskItem (icon, | |
3591 * label and action). | |
3592 * | |
3593 * @param {Object} defaultItem - taskItem to match. | |
3594 * @param {boolean} isMultiple - if multiple tasks available. | |
3595 */ | |
3596 FileManager.prototype.updateContextMenuActionItems = function(defaultItem, | |
3597 isMultiple) { | |
3598 if (defaultItem) { | |
3599 if (defaultItem.iconType) { | |
3600 this.defaultActionMenuItem_.style.backgroundImage = ''; | |
3601 this.defaultActionMenuItem_.setAttribute('file-type-icon', | |
3602 defaultItem.iconType); | |
3603 } else if (defaultItem.iconUrl) { | |
3604 this.defaultActionMenuItem_.style.backgroundImage = | |
3605 'url(' + defaultItem.iconUrl + ')'; | |
3606 } else { | |
3607 this.defaultActionMenuItem_.style.backgroundImage = ''; | |
3608 } | |
3609 | |
3610 this.defaultActionMenuItem_.label = defaultItem.title; | |
3611 this.defaultActionMenuItem_.disabled = !!defaultItem.disabled; | |
3612 this.defaultActionMenuItem_.taskId = defaultItem.taskId; | |
3613 } | |
3614 | |
3615 var defaultActionSeparator = | |
3616 this.dialogDom_.querySelector('#default-action-separator'); | |
3617 | |
3618 this.openWithCommand_.canExecuteChange(); | |
3619 this.openWithCommand_.setHidden(!(defaultItem && isMultiple)); | |
3620 this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled; | |
3621 | |
3622 this.defaultActionMenuItem_.hidden = !defaultItem; | |
3623 defaultActionSeparator.hidden = !defaultItem; | |
3624 }; | |
3625 | |
3626 | |
3627 /** | |
3628 * Window beforeunload handler. | |
3629 * @return {string} Message to show. Ignored when running as a packaged app. | |
3630 * @private | |
3631 */ | |
3632 FileManager.prototype.onBeforeUnload_ = function() { | |
3633 if (this.filePopup_ && | |
3634 this.filePopup_.contentWindow && | |
3635 this.filePopup_.contentWindow.beforeunload) { | |
3636 // The gallery might want to prevent the unload if it is busy. | |
3637 return this.filePopup_.contentWindow.beforeunload(); | |
3638 } | |
3639 return null; | |
3640 }; | |
3641 | |
3642 /** | |
3643 * @return {FileSelection} Selection object. | |
3644 */ | |
3645 FileManager.prototype.getSelection = function() { | |
3646 return this.selectionHandler_.selection; | |
3647 }; | |
3648 | |
3649 /** | |
3650 * @return {ArrayDataModel} File list. | |
3651 */ | |
3652 FileManager.prototype.getFileList = function() { | |
3653 return this.directoryModel_.getFileList(); | |
3654 }; | |
3655 | |
3656 /** | |
3657 * @return {cr.ui.List} Current list object. | |
3658 */ | |
3659 FileManager.prototype.getCurrentList = function() { | |
3660 return this.currentList_; | |
3661 }; | |
3662 | |
3663 /** | |
3664 * Retrieve the preferences of the files.app. This method caches the result | |
3665 * and returns it unless opt_update is true. | |
3666 * @param {function(Object.<string, *>)} callback Callback to get the | |
3667 * preference. | |
3668 * @param {boolean=} opt_update If is's true, don't use the cache and | |
3669 * retrieve latest preference. Default is false. | |
3670 * @private | |
3671 */ | |
3672 FileManager.prototype.getPreferences_ = function(callback, opt_update) { | |
3673 if (!opt_update && this.preferences_ !== undefined) { | |
3674 callback(this.preferences_); | |
3675 return; | |
3676 } | |
3677 | |
3678 chrome.fileBrowserPrivate.getPreferences(function(prefs) { | |
3679 this.preferences_ = prefs; | |
3680 callback(prefs); | |
3681 }.bind(this)); | |
3682 }; | |
3683 })(); | |
OLD | NEW |