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