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