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