Upstream version 10.39.225.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             // By default, the selection should be selected entry and the
1988             // parent directory of it should be the current directory.
1989             if (!nextCurrentDirEntry)
1990               selectionEntry = inEntry;
1991
1992             callback();
1993           }.bind(this), callback);
1994     }.bind(this));
1995     // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done
1996     // by the previous step).
1997     queue.run(function(callback) {
1998       if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) {
1999         callback();
2000         return;
2001       }
2002       webkitResolveLocalFileSystemURL(
2003           this.initCurrentDirectoryURL_,
2004           function(inEntry) {
2005             var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
2006             if (!locationInfo) {
2007               callback();
2008               return;
2009             }
2010             nextCurrentDirEntry = inEntry;
2011             callback();
2012           }.bind(this), callback);
2013       // TODO(mtomasz): Implement reopening on special search, when fake
2014       // entries are converted to directory providers.
2015     }.bind(this));
2016
2017     // If the directory to be changed to is not available, then first fallback
2018     // to the parent of the selection entry.
2019     queue.run(function(callback) {
2020       if (nextCurrentDirEntry || !selectionEntry) {
2021         callback();
2022         return;
2023       }
2024       selectionEntry.getParent(function(inEntry) {
2025         nextCurrentDirEntry = inEntry;
2026         callback();
2027       }.bind(this));
2028     }.bind(this));
2029
2030     // If the directory to be changed to is still not resolved, then fallback
2031     // to the default display root.
2032     queue.run(function(callback) {
2033       if (nextCurrentDirEntry) {
2034         callback();
2035         return;
2036       }
2037       this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) {
2038         nextCurrentDirEntry = displayRoot;
2039         callback();
2040       }.bind(this));
2041     }.bind(this));
2042
2043     // If selection failed to be resolved (eg. didn't exist, in case of saving
2044     // a file, or in case of a fallback of the current directory, then try to
2045     // resolve again using the target name.
2046     queue.run(function(callback) {
2047       if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) {
2048         callback();
2049         return;
2050       }
2051       // Try to resolve as a file first. If it fails, then as a directory.
2052       nextCurrentDirEntry.getFile(
2053           this.initTargetName_,
2054           {},
2055           function(targetEntry) {
2056             selectionEntry = targetEntry;
2057             callback();
2058           }, function() {
2059             // Failed to resolve as a file
2060             nextCurrentDirEntry.getDirectory(
2061                 this.initTargetName_,
2062                 {},
2063                 function(targetEntry) {
2064                   selectionEntry = targetEntry;
2065                   callback();
2066                 }, function() {
2067                   // Failed to resolve as either file or directory.
2068                   callback();
2069                 });
2070           }.bind(this));
2071     }.bind(this));
2072
2073
2074     // Finalize.
2075     queue.run(function(callback) {
2076       // Check directory change.
2077       tracker.stop();
2078       if (tracker.hasChanged) {
2079         callback();
2080         return;
2081       }
2082       // Finish setup current directory.
2083       this.finishSetupCurrentDirectory_(
2084           nextCurrentDirEntry,
2085           selectionEntry,
2086           this.initTargetName_);
2087       callback();
2088     }.bind(this));
2089   };
2090
2091   /**
2092    * @param {DirectoryEntry} directoryEntry Directory to be opened.
2093    * @param {Entry=} opt_selectionEntry Entry to be selected.
2094    * @param {string=} opt_suggestedName Suggested name for a non-existing\
2095    *     selection.
2096    * @private
2097    */
2098   FileManager.prototype.finishSetupCurrentDirectory_ = function(
2099       directoryEntry, opt_selectionEntry, opt_suggestedName) {
2100     // Open the directory, and select the selection (if passed).
2101     if (util.isFakeEntry(directoryEntry)) {
2102       this.directoryModel_.specialSearch(directoryEntry, '');
2103     } else {
2104       this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
2105         if (opt_selectionEntry)
2106           this.directoryModel_.selectEntry(opt_selectionEntry);
2107       }.bind(this));
2108     }
2109
2110     if (this.dialogType === DialogType.FULL_PAGE) {
2111       // In the FULL_PAGE mode if the restored URL points to a file we might
2112       // have to invoke a task after selecting it.
2113       if (this.params_.action === 'select')
2114         return;
2115
2116       var task = null;
2117
2118       // TODO(mtomasz): Implement remounting archives after crash.
2119       //                See: crbug.com/333139
2120
2121       // If there is a task to be run, run it after the scan is completed.
2122       if (task) {
2123         var listener = function() {
2124           if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(),
2125                                 directoryEntry)) {
2126             // Opened on a different URL. Probably fallbacked. Therefore,
2127             // do not invoke a task.
2128             return;
2129           }
2130           this.directoryModel_.removeEventListener(
2131               'scan-completed', listener);
2132           task();
2133         }.bind(this);
2134         this.directoryModel_.addEventListener('scan-completed', listener);
2135       }
2136     } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
2137       this.filenameInput_.value = opt_suggestedName || '';
2138       this.selectTargetNameInFilenameInput_();
2139     }
2140   };
2141
2142   /**
2143    * @private
2144    */
2145   FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
2146     var entries = this.directoryModel_.getFileList().slice();
2147     var directoryEntry = this.directoryModel_.getCurrentDirEntry();
2148     if (!directoryEntry)
2149       return;
2150     // We don't pass callback here. When new metadata arrives, we have an
2151     // observer registered to update the UI.
2152
2153     // TODO(dgozman): refresh content metadata only when modificationTime
2154     // changed.
2155     var isFakeEntry = util.isFakeEntry(directoryEntry);
2156     var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
2157     if (!isFakeEntry)
2158       this.metadataCache_.clearRecursively(directoryEntry, '*');
2159     this.metadataCache_.get(getEntries, 'filesystem|external', null);
2160
2161     var visibleItems = this.currentList_.items;
2162     var visibleEntries = [];
2163     for (var i = 0; i < visibleItems.length; i++) {
2164       var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
2165       var entry = this.directoryModel_.getFileList().item(index);
2166       // The following check is a workaround for the bug in list: sometimes item
2167       // does not have listIndex, and therefore is not found in the list.
2168       if (entry) visibleEntries.push(entry);
2169     }
2170     this.metadataCache_.get(visibleEntries, 'thumbnail', null);
2171   };
2172
2173   /**
2174    * @private
2175    */
2176   FileManager.prototype.dailyUpdateModificationTime_ = function() {
2177     var entries = this.directoryModel_.getFileList().slice();
2178     this.metadataCache_.get(
2179         entries,
2180         'filesystem',
2181         function() {
2182           this.updateMetadataInUI_('filesystem', entries);
2183         }.bind(this));
2184
2185     setTimeout(this.dailyUpdateModificationTime_.bind(this),
2186                MILLISECONDS_IN_DAY);
2187   };
2188
2189   /**
2190    * @param {string} type Type of metadata changed.
2191    * @param {Array.<Entry>} entries Array of entries.
2192    * @private
2193    */
2194   FileManager.prototype.updateMetadataInUI_ = function(type, entries) {
2195     if (this.listType_ == FileManager.ListType.DETAIL)
2196       this.table_.updateListItemsMetadata(type, entries);
2197     else
2198       this.grid_.updateListItemsMetadata(type, entries);
2199     // TODO: update bottom panel thumbnails.
2200   };
2201
2202   /**
2203    * Restore the item which is being renamed while refreshing the file list. Do
2204    * nothing if no item is being renamed or such an item disappeared.
2205    *
2206    * While refreshing file list it gets repopulated with new file entries.
2207    * There is not a big difference whether DOM items stay the same or not.
2208    * Except for the item that the user is renaming.
2209    *
2210    * @private
2211    */
2212   FileManager.prototype.restoreItemBeingRenamed_ = function() {
2213     if (!this.isRenamingInProgress())
2214       return;
2215
2216     var dm = this.directoryModel_;
2217     var leadIndex = dm.getFileListSelection().leadIndex;
2218     if (leadIndex < 0)
2219       return;
2220
2221     var leadEntry = dm.getFileList().item(leadIndex);
2222     if (!util.isSameEntry(this.renameInput_.currentEntry, leadEntry))
2223       return;
2224
2225     var leadListItem = this.findListItemForNode_(this.renameInput_);
2226     if (this.currentList_ == this.table_.list) {
2227       this.table_.updateFileMetadata(leadListItem, leadEntry);
2228     }
2229     this.currentList_.restoreLeadItem(leadListItem);
2230   };
2231
2232   /**
2233    * TODO(mtomasz): Move this to a utility function working on the root type.
2234    * @return {boolean} True if the current directory content is from Google
2235    *     Drive.
2236    */
2237   FileManager.prototype.isOnDrive = function() {
2238     var rootType = this.directoryModel_.getCurrentRootType();
2239     return rootType === VolumeManagerCommon.RootType.DRIVE ||
2240            rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME ||
2241            rootType === VolumeManagerCommon.RootType.DRIVE_RECENT ||
2242            rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE;
2243   };
2244
2245   /**
2246    * Check if the drive-related setting items should be shown on currently
2247    * displayed gear menu.
2248    * @return {boolean} True if those setting items should be shown.
2249    */
2250   FileManager.prototype.shouldShowDriveSettings = function() {
2251     return this.isOnDrive() && this.isSecretGearMenuShown_;
2252   };
2253
2254   /**
2255    * Overrides default handling for clicks on hyperlinks.
2256    * In a packaged apps links with targer='_blank' open in a new tab by
2257    * default, other links do not open at all.
2258    *
2259    * @param {Event} event Click event.
2260    * @private
2261    */
2262   FileManager.prototype.onExternalLinkClick_ = function(event) {
2263     if (event.target.tagName != 'A' || !event.target.href)
2264       return;
2265
2266     if (this.dialogType != DialogType.FULL_PAGE)
2267       this.onCancel_();
2268   };
2269
2270   /**
2271    * Task combobox handler.
2272    *
2273    * @param {Object} event Event containing task which was clicked.
2274    * @private
2275    */
2276   FileManager.prototype.onTaskItemClicked_ = function(event) {
2277     var selection = this.getSelection();
2278     if (!selection.tasks) return;
2279
2280     if (event.item.task) {
2281       // Task field doesn't exist on change-default dropdown item.
2282       selection.tasks.execute(event.item.task.taskId);
2283     } else {
2284       var extensions = [];
2285
2286       for (var i = 0; i < selection.entries.length; i++) {
2287         var match = /\.(\w+)$/g.exec(selection.entries[i].toURL());
2288         if (match) {
2289           var ext = match[1].toUpperCase();
2290           if (extensions.indexOf(ext) == -1) {
2291             extensions.push(ext);
2292           }
2293         }
2294       }
2295
2296       var format = '';
2297
2298       if (extensions.length == 1) {
2299         format = extensions[0];
2300       }
2301
2302       // Change default was clicked. We should open "change default" dialog.
2303       selection.tasks.showTaskPicker(this.defaultTaskPicker,
2304           loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
2305           strf('CHANGE_DEFAULT_CAPTION', format),
2306           this.onDefaultTaskDone_.bind(this));
2307     }
2308   };
2309
2310   /**
2311    * Sets the given task as default, when this task is applicable.
2312    *
2313    * @param {Object} task Task to set as default.
2314    * @private
2315    */
2316   FileManager.prototype.onDefaultTaskDone_ = function(task) {
2317     // TODO(dgozman): move this method closer to tasks.
2318     var selection = this.getSelection();
2319     // TODO(mtomasz): Move conversion from entry to url to custom bindings.
2320     // crbug.com/345527.
2321     chrome.fileManagerPrivate.setDefaultTask(
2322         task.taskId,
2323         util.entriesToURLs(selection.entries),
2324         selection.mimeTypes);
2325     selection.tasks = new FileTasks(this);
2326     selection.tasks.init(selection.entries, selection.mimeTypes);
2327     selection.tasks.display(this.taskItems_);
2328     this.refreshCurrentDirectoryMetadata_();
2329     this.selectionHandler_.onFileSelectionChanged();
2330   };
2331
2332   /**
2333    * @private
2334    */
2335   FileManager.prototype.onPreferencesChanged_ = function() {
2336     var self = this;
2337     this.getPreferences_(function(prefs) {
2338       self.initDateTimeFormatters_();
2339       self.refreshCurrentDirectoryMetadata_();
2340
2341       if (prefs.cellularDisabled)
2342         self.syncButton.setAttribute('checked', '');
2343       else
2344         self.syncButton.removeAttribute('checked');
2345
2346       if (self.hostedButton.hasAttribute('checked') ===
2347           prefs.hostedFilesDisabled && self.isOnDrive()) {
2348         self.directoryModel_.rescan(false);
2349       }
2350
2351       if (!prefs.hostedFilesDisabled)
2352         self.hostedButton.setAttribute('checked', '');
2353       else
2354         self.hostedButton.removeAttribute('checked');
2355     },
2356     true /* refresh */);
2357   };
2358
2359   FileManager.prototype.onDriveConnectionChanged_ = function() {
2360     var connection = this.volumeManager_.getDriveConnectionState();
2361     if (this.commandHandler)
2362       this.commandHandler.updateAvailability();
2363     if (this.dialogContainer_)
2364       this.dialogContainer_.setAttribute('connection', connection.type);
2365     this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
2366     this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
2367   };
2368
2369   /**
2370    * Tells whether the current directory is read only.
2371    * TODO(mtomasz): Remove and use EntryLocation directly.
2372    * @return {boolean} True if read only, false otherwise.
2373    */
2374   FileManager.prototype.isOnReadonlyDirectory = function() {
2375     return this.directoryModel_.isReadOnly();
2376   };
2377
2378   /**
2379    * @param {Event} event Unmount event.
2380    * @private
2381    */
2382   FileManager.prototype.onExternallyUnmounted_ = function(event) {
2383     if (event.volumeInfo === this.currentVolumeInfo_) {
2384       if (this.closeOnUnmount_) {
2385         // If the file manager opened automatically when a usb drive inserted,
2386         // user have never changed current volume (that implies the current
2387         // directory is still on the device) then close this window.
2388         window.close();
2389       }
2390     }
2391   };
2392
2393   /**
2394    * @return {Array.<Entry>} List of all entries in the current directory.
2395    */
2396   FileManager.prototype.getAllEntriesInCurrentDirectory = function() {
2397     return this.directoryModel_.getFileList().slice();
2398   };
2399
2400   FileManager.prototype.isRenamingInProgress = function() {
2401     return !!this.renameInput_.currentEntry;
2402   };
2403
2404   /**
2405    * @private
2406    */
2407   FileManager.prototype.focusCurrentList_ = function() {
2408     if (this.listType_ == FileManager.ListType.DETAIL)
2409       this.table_.focus();
2410     else  // this.listType_ == FileManager.ListType.THUMBNAIL)
2411       this.grid_.focus();
2412   };
2413
2414   /**
2415    * Return DirectoryEntry of the current directory or null.
2416    * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
2417    *     null if the directory model is not ready or the current directory is
2418    *     not set.
2419    */
2420   FileManager.prototype.getCurrentDirectoryEntry = function() {
2421     return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
2422   };
2423
2424   /**
2425    * Shows the share dialog for the selected file or directory.
2426    */
2427   FileManager.prototype.shareSelection = function() {
2428     var entries = this.getSelection().entries;
2429     if (entries.length != 1) {
2430       console.warn('Unable to share multiple items at once.');
2431       return;
2432     }
2433     // Add the overlapped class to prevent the applicaiton window from
2434     // captureing mouse events.
2435     this.shareDialog_.show(entries[0], function(result) {
2436       if (result == ShareDialog.Result.NETWORK_ERROR)
2437         this.error.show(str('SHARE_ERROR'));
2438     }.bind(this));
2439   };
2440
2441   /**
2442    * Creates a folder shortcut.
2443    * @param {Entry} entry A shortcut which refers to |entry| to be created.
2444    */
2445   FileManager.prototype.createFolderShortcut = function(entry) {
2446     // Duplicate entry.
2447     if (this.folderShortcutExists(entry))
2448       return;
2449
2450     this.folderShortcutsModel_.add(entry);
2451   };
2452
2453   /**
2454    * Checkes if the shortcut which refers to the given folder exists or not.
2455    * @param {Entry} entry Entry of the folder to be checked.
2456    */
2457   FileManager.prototype.folderShortcutExists = function(entry) {
2458     return this.folderShortcutsModel_.exists(entry);
2459   };
2460
2461   /**
2462    * Removes the folder shortcut.
2463    * @param {Entry} entry The shortcut which refers to |entry| is to be removed.
2464    */
2465   FileManager.prototype.removeFolderShortcut = function(entry) {
2466     this.folderShortcutsModel_.remove(entry);
2467   };
2468
2469   /**
2470    * Blinks the selection. Used to give feedback when copying or cutting the
2471    * selection.
2472    */
2473   FileManager.prototype.blinkSelection = function() {
2474     var selection = this.getSelection();
2475     if (!selection || selection.totalCount == 0)
2476       return;
2477
2478     for (var i = 0; i < selection.entries.length; i++) {
2479       var selectedIndex = selection.indexes[i];
2480       var listItem = this.currentList_.getListItemByIndex(selectedIndex);
2481       if (listItem)
2482         this.blinkListItem_(listItem);
2483     }
2484   };
2485
2486   /**
2487    * @param {Element} listItem List item element.
2488    * @private
2489    */
2490   FileManager.prototype.blinkListItem_ = function(listItem) {
2491     listItem.classList.add('blink');
2492     setTimeout(function() {
2493       listItem.classList.remove('blink');
2494     }, 100);
2495   };
2496
2497   /**
2498    * @private
2499    */
2500   FileManager.prototype.selectTargetNameInFilenameInput_ = function() {
2501     var input = this.filenameInput_;
2502     input.focus();
2503     var selectionEnd = input.value.lastIndexOf('.');
2504     if (selectionEnd == -1) {
2505       input.select();
2506     } else {
2507       input.selectionStart = 0;
2508       input.selectionEnd = selectionEnd;
2509     }
2510   };
2511
2512   /**
2513    * Handles mouse click or tap.
2514    *
2515    * @param {Event} event The click event.
2516    * @private
2517    */
2518   FileManager.prototype.onDetailClick_ = function(event) {
2519     if (this.isRenamingInProgress()) {
2520       // Don't pay attention to clicks during a rename.
2521       return;
2522     }
2523
2524     var listItem = this.findListItemForEvent_(event);
2525     var selection = this.getSelection();
2526     if (!listItem || !listItem.selected || selection.totalCount != 1) {
2527       return;
2528     }
2529
2530     // React on double click, but only if both clicks hit the same item.
2531     // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2532     var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
2533     this.lastClickedItem_ = listItem;
2534
2535     if (event.detail != clickNumber)
2536       return;
2537
2538     var entry = selection.entries[0];
2539     if (entry.isDirectory) {
2540       this.onDirectoryAction_(entry);
2541     } else {
2542       this.dispatchSelectionAction_();
2543     }
2544   };
2545
2546   /**
2547    * @private
2548    */
2549   FileManager.prototype.dispatchSelectionAction_ = function() {
2550     if (this.dialogType == DialogType.FULL_PAGE) {
2551       var selection = this.getSelection();
2552       var tasks = selection.tasks;
2553       var urls = selection.urls;
2554       var mimeTypes = selection.mimeTypes;
2555       if (tasks)
2556         tasks.executeDefault();
2557       return true;
2558     }
2559     if (!this.okButton_.disabled) {
2560       this.onOk_();
2561       return true;
2562     }
2563     return false;
2564   };
2565
2566   /**
2567    * Opens the suggest file dialog.
2568    *
2569    * @param {Entry} entry Entry of the file.
2570    * @param {function()} onSuccess Success callback.
2571    * @param {function()} onCancelled User-cancelled callback.
2572    * @param {function()} onFailure Failure callback.
2573    * @private
2574    */
2575   FileManager.prototype.openSuggestAppsDialog =
2576       function(entry, onSuccess, onCancelled, onFailure) {
2577     if (!url) {
2578       onFailure();
2579       return;
2580     }
2581
2582     this.metadataCache_.getOne(entry, 'external', function(prop) {
2583       if (!prop || !prop.contentMimeType) {
2584         onFailure();
2585         return;
2586       }
2587
2588       var basename = entry.name;
2589       var splitted = util.splitExtension(basename);
2590       var filename = splitted[0];
2591       var extension = splitted[1];
2592       var mime = prop.contentMimeType;
2593
2594       // Returns with failure if the file has neither extension nor mime.
2595       if (!extension || !mime) {
2596         onFailure();
2597         return;
2598       }
2599
2600       var onDialogClosed = function(result) {
2601         switch (result) {
2602           case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
2603             onSuccess();
2604             break;
2605           case SuggestAppsDialog.Result.FAILED:
2606             onFailure();
2607             break;
2608           default:
2609             onCancelled();
2610         }
2611       };
2612
2613       if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
2614         this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
2615       } else {
2616         this.suggestAppsDialog.showByExtensionAndMime(
2617             extension, mime, onDialogClosed);
2618       }
2619     }.bind(this));
2620   };
2621
2622   /**
2623    * Called when a dialog is shown or hidden.
2624    * @param {boolean} show True if a dialog is shown, false if hidden.
2625    */
2626   FileManager.prototype.onDialogShownOrHidden = function(show) {
2627     if (show) {
2628       // If a dialog is shown, activate the window.
2629       var appWindow = chrome.app.window.current();
2630       if (appWindow)
2631         appWindow.focus();
2632     }
2633
2634     // Set/unset a flag to disable dragging on the title area.
2635     this.dialogContainer_.classList.toggle('disable-header-drag', show);
2636   };
2637
2638   /**
2639    * Executes directory action (i.e. changes directory).
2640    *
2641    * @param {DirectoryEntry} entry Directory entry to which directory should be
2642    *                               changed.
2643    * @private
2644    */
2645   FileManager.prototype.onDirectoryAction_ = function(entry) {
2646     return this.directoryModel_.changeDirectoryEntry(entry);
2647   };
2648
2649   /**
2650    * Update the window title.
2651    * @private
2652    */
2653   FileManager.prototype.updateTitle_ = function() {
2654     if (this.dialogType != DialogType.FULL_PAGE)
2655       return;
2656
2657     if (!this.currentVolumeInfo_)
2658       return;
2659
2660     this.document_.title = this.currentVolumeInfo_.label;
2661   };
2662
2663   /**
2664    * Updates the location information displayed on the toolbar.
2665    * @param {DirectoryEntry=} opt_entry Directory entry to be displayed as
2666    *     current location. Default entry is the current directory.
2667    * @private
2668    */
2669   FileManager.prototype.updateLocationLine_ = function(opt_entry) {
2670     var entry = opt_entry || this.getCurrentDirectoryEntry();
2671     // Updates volume icon.
2672     var location = this.volumeManager_.getLocationInfo(entry);
2673     if (location && location.rootType && location.isRootEntry) {
2674       this.ui_.locationVolumeIcon.setAttribute(
2675           'volume-type-icon', location.rootType);
2676       this.ui_.locationVolumeIcon.removeAttribute('volume-subtype');
2677     } else {
2678       this.ui_.locationVolumeIcon.setAttribute(
2679           'volume-type-icon', location.volumeInfo.volumeType);
2680       this.ui_.locationVolumeIcon.setAttribute(
2681           'volume-subtype', location.volumeInfo.deviceType);
2682     }
2683     // Updates breadcrumbs.
2684     this.ui_.locationBreadcrumbs.show(entry);
2685   };
2686
2687   /**
2688    * Update the gear menu.
2689    * @private
2690    */
2691   FileManager.prototype.updateGearMenu_ = function() {
2692     this.refreshRemainingSpace_(true);  // Show loading caption.
2693   };
2694
2695   /**
2696    * Refreshes space info of the current volume.
2697    * @param {boolean} showLoadingCaption Whether show loading caption or not.
2698    * @private
2699    */
2700   FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
2701     if (!this.currentVolumeInfo_)
2702       return;
2703
2704     var volumeSpaceInfo =
2705         this.dialogDom_.querySelector('#volume-space-info');
2706     var volumeSpaceInfoSeparator =
2707         this.dialogDom_.querySelector('#volume-space-info-separator');
2708     var volumeSpaceInfoLabel =
2709         this.dialogDom_.querySelector('#volume-space-info-label');
2710     var volumeSpaceInnerBar =
2711         this.dialogDom_.querySelector('#volume-space-info-bar');
2712     var volumeSpaceOuterBar =
2713         this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
2714
2715     var currentVolumeInfo = this.currentVolumeInfo_;
2716
2717     // TODO(mtomasz): Add support for remaining space indication for provided
2718     // file systems.
2719     if (currentVolumeInfo.volumeType ==
2720         VolumeManagerCommon.VolumeType.PROVIDED) {
2721       volumeSpaceInfo.hidden = true;
2722       volumeSpaceInfoSeparator.hidden = true;
2723       return;
2724     }
2725
2726     volumeSpaceInfo.hidden = false;
2727     volumeSpaceInfoSeparator.hidden = false;
2728     volumeSpaceInnerBar.setAttribute('pending', '');
2729
2730     if (showLoadingCaption) {
2731       volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2732       volumeSpaceInnerBar.style.width = '100%';
2733     }
2734
2735     chrome.fileManagerPrivate.getSizeStats(
2736         currentVolumeInfo.volumeId, function(result) {
2737           var volumeInfo = this.volumeManager_.getVolumeInfo(
2738               this.directoryModel_.getCurrentDirEntry());
2739           if (currentVolumeInfo !== this.currentVolumeInfo_)
2740             return;
2741           updateSpaceInfo(result,
2742                           volumeSpaceInnerBar,
2743                           volumeSpaceInfoLabel,
2744                           volumeSpaceOuterBar);
2745         }.bind(this));
2746   };
2747
2748   /**
2749    * Update the UI when the current directory changes.
2750    *
2751    * @param {Event} event The directory-changed event.
2752    * @private
2753    */
2754   FileManager.prototype.onDirectoryChanged_ = function(event) {
2755     var oldCurrentVolumeInfo = this.currentVolumeInfo_;
2756
2757     // Remember the current volume info.
2758     this.currentVolumeInfo_ = this.volumeManager_.getVolumeInfo(
2759         event.newDirEntry);
2760
2761     // If volume has changed, then update the gear menu.
2762     if (oldCurrentVolumeInfo !== this.currentVolumeInfo_) {
2763       this.updateGearMenu_();
2764       // If the volume has changed, and it was previously set, then do not
2765       // close on unmount anymore.
2766       if (oldCurrentVolumeInfo)
2767         this.closeOnUnmount_ = false;
2768     }
2769
2770     this.selectionHandler_.onFileSelectionChanged();
2771     this.ui_.searchBox.clear();
2772     // TODO(mtomasz): Consider remembering the selection.
2773     util.updateAppState(
2774         this.getCurrentDirectoryEntry() ?
2775         this.getCurrentDirectoryEntry().toURL() : '',
2776         '' /* selectionURL */,
2777         '' /* opt_param */);
2778
2779     if (this.commandHandler)
2780       this.commandHandler.updateAvailability();
2781
2782     this.updateUnformattedVolumeStatus_();
2783     this.updateTitle_();
2784     this.updateLocationLine_();
2785
2786     var currentEntry = this.getCurrentDirectoryEntry();
2787     this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ?
2788         null : currentEntry;
2789   };
2790
2791   FileManager.prototype.updateUnformattedVolumeStatus_ = function() {
2792     var volumeInfo = this.volumeManager_.getVolumeInfo(
2793         this.directoryModel_.getCurrentDirEntry());
2794
2795     if (volumeInfo && volumeInfo.error) {
2796       this.dialogDom_.setAttribute('unformatted', '');
2797
2798       var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2799       if (volumeInfo.error ===
2800           VolumeManagerCommon.VolumeError.UNSUPPORTED_FILESYSTEM) {
2801         errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2802       } else {
2803         errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2804       }
2805
2806       // Update 'canExecute' for format command so the format button's disabled
2807       // property is properly set.
2808       if (this.commandHandler)
2809         this.commandHandler.updateAvailability();
2810     } else {
2811       this.dialogDom_.removeAttribute('unformatted');
2812     }
2813   };
2814
2815   FileManager.prototype.findListItemForEvent_ = function(event) {
2816     return this.findListItemForNode_(event.touchedElement || event.srcElement);
2817   };
2818
2819   FileManager.prototype.findListItemForNode_ = function(node) {
2820     var item = this.currentList_.getListItemAncestor(node);
2821     // TODO(serya): list should check that.
2822     return item && this.currentList_.isItem(item) ? item : null;
2823   };
2824
2825   /**
2826    * Unload handler for the page.
2827    * @private
2828    */
2829   FileManager.prototype.onUnload_ = function() {
2830     if (this.directoryModel_)
2831       this.directoryModel_.dispose();
2832     if (this.volumeManager_)
2833       this.volumeManager_.dispose();
2834     for (var i = 0;
2835          i < this.fileTransferController_.pendingTaskIds.length;
2836          i++) {
2837       var taskId = this.fileTransferController_.pendingTaskIds[i];
2838       var item =
2839           this.backgroundPage_.background.progressCenter.getItemById(taskId);
2840       item.message = '';
2841       item.state = ProgressItemState.CANCELED;
2842       this.backgroundPage_.background.progressCenter.updateItem(item);
2843     }
2844     if (this.progressCenterPanel_) {
2845       this.backgroundPage_.background.progressCenter.removePanel(
2846           this.progressCenterPanel_);
2847     }
2848     if (this.fileOperationManager_) {
2849       if (this.onCopyProgressBound_) {
2850         this.fileOperationManager_.removeEventListener(
2851             'copy-progress', this.onCopyProgressBound_);
2852       }
2853       if (this.onEntriesChangedBound_) {
2854         this.fileOperationManager_.removeEventListener(
2855             'entries-changed', this.onEntriesChangedBound_);
2856       }
2857     }
2858     window.closing = true;
2859     if (this.backgroundPage_)
2860       this.backgroundPage_.background.tryClose();
2861   };
2862
2863   FileManager.prototype.initiateRename = function() {
2864     var item = this.currentList_.ensureLeadItemExists();
2865     if (!item)
2866       return;
2867     var label = item.querySelector('.filename-label');
2868     var input = this.renameInput_;
2869     var currentEntry = this.currentList_.dataModel.item(item.listIndex);
2870
2871     input.value = label.textContent;
2872     item.setAttribute('renaming', '');
2873     label.parentNode.appendChild(input);
2874     input.focus();
2875
2876     var selectionEnd = input.value.lastIndexOf('.');
2877     if (currentEntry.isFile && selectionEnd !== -1) {
2878       input.selectionStart = 0;
2879       input.selectionEnd = selectionEnd;
2880     } else {
2881       input.select();
2882     }
2883
2884     // This has to be set late in the process so we don't handle spurious
2885     // blur events.
2886     input.currentEntry = currentEntry;
2887     this.table_.startBatchUpdates();
2888     this.grid_.startBatchUpdates();
2889   };
2890
2891   /**
2892    * @type {Event} Key event.
2893    * @private
2894    */
2895   FileManager.prototype.onRenameInputKeyDown_ = function(event) {
2896     if (!this.isRenamingInProgress())
2897       return;
2898
2899     // Do not move selection or lead item in list during rename.
2900     if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
2901       event.stopPropagation();
2902     }
2903
2904     switch (util.getKeyModifiers(event) + event.keyIdentifier) {
2905       case 'U+001B':  // Escape
2906         this.cancelRename_();
2907         event.preventDefault();
2908         break;
2909
2910       case 'Enter':
2911         this.commitRename_();
2912         event.preventDefault();
2913         break;
2914     }
2915   };
2916
2917   /**
2918    * @type {Event} Blur event.
2919    * @private
2920    */
2921   FileManager.prototype.onRenameInputBlur_ = function(event) {
2922     if (this.isRenamingInProgress() && !this.renameInput_.validation_)
2923       this.commitRename_();
2924   };
2925
2926   /**
2927    * @private
2928    */
2929   FileManager.prototype.commitRename_ = function() {
2930     var input = this.renameInput_;
2931     var entry = input.currentEntry;
2932     var newName = input.value;
2933
2934     if (newName == entry.name) {
2935       this.cancelRename_();
2936       return;
2937     }
2938
2939     var renamedItemElement = this.findListItemForNode_(this.renameInput_);
2940     var nameNode = renamedItemElement.querySelector('.filename-label');
2941
2942     input.validation_ = true;
2943     var validationDone = function(valid) {
2944       input.validation_ = false;
2945
2946       if (!valid) {
2947         // Cancel rename if it fails to restore focus from alert dialog.
2948         // Otherwise, just cancel the commitment and continue to rename.
2949         if (this.document_.activeElement != input)
2950           this.cancelRename_();
2951         return;
2952       }
2953
2954       // Validation succeeded. Do renaming.
2955       this.renameInput_.currentEntry = null;
2956       if (this.renameInput_.parentNode)
2957         this.renameInput_.parentNode.removeChild(this.renameInput_);
2958       renamedItemElement.setAttribute('renaming', 'provisional');
2959
2960       // Optimistically apply new name immediately to avoid flickering in
2961       // case of success.
2962       nameNode.textContent = newName;
2963
2964       util.rename(
2965           entry, newName,
2966           function(newEntry) {
2967             this.directoryModel_.onRenameEntry(entry, newEntry);
2968             renamedItemElement.removeAttribute('renaming');
2969             this.table_.endBatchUpdates();
2970             this.grid_.endBatchUpdates();
2971             // Focus may go out of the list. Back it to the list.
2972             this.currentList_.focus();
2973           }.bind(this),
2974           function(error) {
2975             // Write back to the old name.
2976             nameNode.textContent = entry.name;
2977             renamedItemElement.removeAttribute('renaming');
2978             this.table_.endBatchUpdates();
2979             this.grid_.endBatchUpdates();
2980
2981             // Show error dialog.
2982             var message;
2983             if (error.name == util.FileError.PATH_EXISTS_ERR ||
2984                 error.name == util.FileError.TYPE_MISMATCH_ERR) {
2985               // Check the existing entry is file or not.
2986               // 1) If the entry is a file:
2987               //   a) If we get PATH_EXISTS_ERR, a file exists.
2988               //   b) If we get TYPE_MISMATCH_ERR, a directory exists.
2989               // 2) If the entry is a directory:
2990               //   a) If we get PATH_EXISTS_ERR, a directory exists.
2991               //   b) If we get TYPE_MISMATCH_ERR, a file exists.
2992               message = strf(
2993                   (entry.isFile && error.name ==
2994                       util.FileError.PATH_EXISTS_ERR) ||
2995                   (!entry.isFile && error.name ==
2996                       util.FileError.TYPE_MISMATCH_ERR) ?
2997                       'FILE_ALREADY_EXISTS' :
2998                       'DIRECTORY_ALREADY_EXISTS',
2999                   newName);
3000             } else {
3001               message = strf('ERROR_RENAMING', entry.name,
3002                              util.getFileErrorString(error.name));
3003             }
3004
3005             this.alert.show(message);
3006           }.bind(this));
3007     };
3008
3009     // TODO(haruki): this.getCurrentDirectoryEntry() might not return the actual
3010     // parent if the directory content is a search result. Fix it to do proper
3011     // validation.
3012     this.validateFileName_(this.getCurrentDirectoryEntry(),
3013                            newName,
3014                            validationDone.bind(this));
3015   };
3016
3017   /**
3018    * @private
3019    */
3020   FileManager.prototype.cancelRename_ = function() {
3021     this.renameInput_.currentEntry = null;
3022
3023     var item = this.findListItemForNode_(this.renameInput_);
3024     if (item)
3025       item.removeAttribute('renaming');
3026
3027     var parent = this.renameInput_.parentNode;
3028     if (parent)
3029       parent.removeChild(this.renameInput_);
3030
3031     this.table_.endBatchUpdates();
3032     this.grid_.endBatchUpdates();
3033
3034     // Focus may go out of the list. Back it to the list.
3035     this.currentList_.focus();
3036   };
3037
3038   /**
3039    * @private
3040    */
3041   FileManager.prototype.onFilenameInputInput_ = function() {
3042     this.selectionHandler_.updateOkButton();
3043   };
3044
3045   /**
3046    * @param {Event} event Key event.
3047    * @private
3048    */
3049   FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
3050     if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */)
3051       this.okButton_.click();
3052   };
3053
3054   /**
3055    * @param {Event} event Focus event.
3056    * @private
3057    */
3058   FileManager.prototype.onFilenameInputFocus_ = function(event) {
3059     var input = this.filenameInput_;
3060
3061     // On focus we want to select everything but the extension, but
3062     // Chrome will select-all after the focus event completes.  We
3063     // schedule a timeout to alter the focus after that happens.
3064     setTimeout(function() {
3065       var selectionEnd = input.value.lastIndexOf('.');
3066       if (selectionEnd == -1) {
3067         input.select();
3068       } else {
3069         input.selectionStart = 0;
3070         input.selectionEnd = selectionEnd;
3071       }
3072     }, 0);
3073   };
3074
3075   /**
3076    * @private
3077    */
3078   FileManager.prototype.onScanStarted_ = function() {
3079     if (this.scanInProgress_) {
3080       this.table_.list.endBatchUpdates();
3081       this.grid_.endBatchUpdates();
3082     }
3083
3084     if (this.commandHandler)
3085       this.commandHandler.updateAvailability();
3086     this.table_.list.startBatchUpdates();
3087     this.grid_.startBatchUpdates();
3088     this.scanInProgress_ = true;
3089
3090     this.scanUpdatedAtLeastOnceOrCompleted_ = false;
3091     if (this.scanCompletedTimer_) {
3092       clearTimeout(this.scanCompletedTimer_);
3093       this.scanCompletedTimer_ = 0;
3094     }
3095
3096     if (this.scanUpdatedTimer_) {
3097       clearTimeout(this.scanUpdatedTimer_);
3098       this.scanUpdatedTimer_ = 0;
3099     }
3100
3101     if (this.spinner_.hidden) {
3102       this.cancelSpinnerTimeout_();
3103       this.showSpinnerTimeout_ =
3104           setTimeout(this.showSpinner_.bind(this, true), 500);
3105     }
3106   };
3107
3108   /**
3109    * @private
3110    */
3111   FileManager.prototype.onScanCompleted_ = function() {
3112     if (!this.scanInProgress_) {
3113       console.error('Scan-completed event recieved. But scan is not started.');
3114       return;
3115     }
3116
3117     if (this.commandHandler)
3118       this.commandHandler.updateAvailability();
3119     this.hideSpinnerLater_();
3120
3121     if (this.scanUpdatedTimer_) {
3122       clearTimeout(this.scanUpdatedTimer_);
3123       this.scanUpdatedTimer_ = 0;
3124     }
3125
3126     // To avoid flickering postpone updating the ui by a small amount of time.
3127     // There is a high chance, that metadata will be received within 50 ms.
3128     this.scanCompletedTimer_ = setTimeout(function() {
3129       // Check if batch updates are already finished by onScanUpdated_().
3130       if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
3131         this.scanUpdatedAtLeastOnceOrCompleted_ = true;
3132       }
3133
3134       this.scanInProgress_ = false;
3135       this.table_.list.endBatchUpdates();
3136       this.grid_.endBatchUpdates();
3137       this.scanCompletedTimer_ = 0;
3138     }.bind(this), 50);
3139   };
3140
3141   /**
3142    * @private
3143    */
3144   FileManager.prototype.onScanUpdated_ = function() {
3145     if (!this.scanInProgress_) {
3146       console.error('Scan-updated event recieved. But scan is not started.');
3147       return;
3148     }
3149
3150     if (this.scanUpdatedTimer_ || this.scanCompletedTimer_)
3151       return;
3152
3153     // Show contents incrementally by finishing batch updated, but only after
3154     // 200ms elapsed, to avoid flickering when it is not necessary.
3155     this.scanUpdatedTimer_ = setTimeout(function() {
3156       // We need to hide the spinner only once.
3157       if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
3158         this.scanUpdatedAtLeastOnceOrCompleted_ = true;
3159         this.hideSpinnerLater_();
3160       }
3161
3162       // Update the UI.
3163       if (this.scanInProgress_) {
3164         this.table_.list.endBatchUpdates();
3165         this.grid_.endBatchUpdates();
3166         this.table_.list.startBatchUpdates();
3167         this.grid_.startBatchUpdates();
3168       }
3169       this.scanUpdatedTimer_ = 0;
3170     }.bind(this), 200);
3171   };
3172
3173   /**
3174    * @private
3175    */
3176   FileManager.prototype.onScanCancelled_ = function() {
3177     if (!this.scanInProgress_) {
3178       console.error('Scan-cancelled event recieved. But scan is not started.');
3179       return;
3180     }
3181
3182     if (this.commandHandler)
3183       this.commandHandler.updateAvailability();
3184     this.hideSpinnerLater_();
3185     if (this.scanCompletedTimer_) {
3186       clearTimeout(this.scanCompletedTimer_);
3187       this.scanCompletedTimer_ = 0;
3188     }
3189     if (this.scanUpdatedTimer_) {
3190       clearTimeout(this.scanUpdatedTimer_);
3191       this.scanUpdatedTimer_ = 0;
3192     }
3193     // Finish unfinished batch updates.
3194     if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
3195       this.scanUpdatedAtLeastOnceOrCompleted_ = true;
3196     }
3197
3198     this.scanInProgress_ = false;
3199     this.table_.list.endBatchUpdates();
3200     this.grid_.endBatchUpdates();
3201   };
3202
3203   /**
3204    * Handle the 'rescan-completed' from the DirectoryModel.
3205    * @private
3206    */
3207   FileManager.prototype.onRescanCompleted_ = function() {
3208     this.selectionHandler_.onFileSelectionChanged();
3209   };
3210
3211   /**
3212    * @private
3213    */
3214   FileManager.prototype.cancelSpinnerTimeout_ = function() {
3215     if (this.showSpinnerTimeout_) {
3216       clearTimeout(this.showSpinnerTimeout_);
3217       this.showSpinnerTimeout_ = 0;
3218     }
3219   };
3220
3221   /**
3222    * @private
3223    */
3224   FileManager.prototype.hideSpinnerLater_ = function() {
3225     this.cancelSpinnerTimeout_();
3226     this.showSpinner_(false);
3227   };
3228
3229   /**
3230    * @param {boolean} on True to show, false to hide.
3231    * @private
3232    */
3233   FileManager.prototype.showSpinner_ = function(on) {
3234     if (on && this.directoryModel_ && this.directoryModel_.isScanning())
3235       this.spinner_.hidden = false;
3236
3237     if (!on && (!this.directoryModel_ ||
3238                 !this.directoryModel_.isScanning() ||
3239                 this.directoryModel_.getFileList().length != 0)) {
3240       this.spinner_.hidden = true;
3241     }
3242   };
3243
3244   FileManager.prototype.createNewFolder = function() {
3245     var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
3246
3247     // Find a name that doesn't exist in the data model.
3248     var files = this.directoryModel_.getFileList();
3249     var hash = {};
3250     for (var i = 0; i < files.length; i++) {
3251       var name = files.item(i).name;
3252       // Filtering names prevents from conflicts with prototype's names
3253       // and '__proto__'.
3254       if (name.substring(0, defaultName.length) == defaultName)
3255         hash[name] = 1;
3256     }
3257
3258     var baseName = defaultName;
3259     var separator = '';
3260     var suffix = '';
3261     var index = '';
3262
3263     var advance = function() {
3264       separator = ' (';
3265       suffix = ')';
3266       index++;
3267     };
3268
3269     var current = function() {
3270       return baseName + separator + index + suffix;
3271     };
3272
3273     // Accessing hasOwnProperty is safe since hash properties filtered.
3274     while (hash.hasOwnProperty(current())) {
3275       advance();
3276     }
3277
3278     var self = this;
3279     var list = self.currentList_;
3280     var tryCreate = function() {
3281     };
3282
3283     var onSuccess = function(entry) {
3284       metrics.recordUserAction('CreateNewFolder');
3285       list.selectedItem = entry;
3286
3287       self.table_.list.endBatchUpdates();
3288       self.grid_.endBatchUpdates();
3289
3290       self.initiateRename();
3291     };
3292
3293     var onError = function(error) {
3294       self.table_.list.endBatchUpdates();
3295       self.grid_.endBatchUpdates();
3296
3297       self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
3298                            util.getFileErrorString(error.name)));
3299     };
3300
3301     var onAbort = function() {
3302       self.table_.list.endBatchUpdates();
3303       self.grid_.endBatchUpdates();
3304     };
3305
3306     this.table_.list.startBatchUpdates();
3307     this.grid_.startBatchUpdates();
3308     this.directoryModel_.createDirectory(current(),
3309                                          onSuccess,
3310                                          onError,
3311                                          onAbort);
3312   };
3313
3314   /**
3315    * Handles click event on the toggle-view button.
3316    * @param {Event} event Click event.
3317    * @private
3318    */
3319   FileManager.prototype.onToggleViewButtonClick_ = function(event) {
3320     if (this.listType_ === FileManager.ListType.DETAIL)
3321       this.setListType(FileManager.ListType.THUMBNAIL);
3322     else
3323       this.setListType(FileManager.ListType.DETAIL);
3324
3325     event.target.blur();
3326   };
3327
3328   /**
3329    * KeyDown event handler for the document.
3330    * @param {Event} event Key event.
3331    * @private
3332    */
3333   FileManager.prototype.onKeyDown_ = function(event) {
3334     if (event.keyCode === 9)  // Tab
3335       this.pressingTab_ = true;
3336     if (event.keyCode === 17)  // Ctrl
3337       this.pressingCtrl_ = true;
3338
3339     if (event.srcElement === this.renameInput_) {
3340       // Ignore keydown handler in the rename input box.
3341       return;
3342     }
3343
3344     switch (util.getKeyModifiers(event) + event.keyIdentifier) {
3345       case 'Ctrl-U+00BE':  // Ctrl-. => Toggle filter files.
3346         this.fileFilter_.setFilterHidden(
3347             !this.fileFilter_.isFilterHiddenOn());
3348         event.preventDefault();
3349         return;
3350
3351       case 'U+001B':  // Escape => Cancel dialog.
3352         if (this.dialogType != DialogType.FULL_PAGE) {
3353           // If there is nothing else for ESC to do, then cancel the dialog.
3354           event.preventDefault();
3355           this.cancelButton_.click();
3356         }
3357         break;
3358     }
3359   };
3360
3361   /**
3362    * KeyUp event handler for the document.
3363    * @param {Event} event Key event.
3364    * @private
3365    */
3366   FileManager.prototype.onKeyUp_ = function(event) {
3367     if (event.keyCode === 9)  // Tab
3368       this.pressingTab_ = false;
3369     if (event.keyCode == 17)  // Ctrl
3370       this.pressingCtrl_ = false;
3371   };
3372
3373   /**
3374    * KeyDown event handler for the div#list-container element.
3375    * @param {Event} event Key event.
3376    * @private
3377    */
3378   FileManager.prototype.onListKeyDown_ = function(event) {
3379     if (event.srcElement.tagName == 'INPUT') {
3380       // Ignore keydown handler in the rename input box.
3381       return;
3382     }
3383
3384     switch (util.getKeyModifiers(event) + event.keyCode) {
3385       case '8':  // Backspace => Up one directory.
3386         event.preventDefault();
3387         // TODO(mtomasz): Use Entry.getParent() instead.
3388         if (!this.getCurrentDirectoryEntry())
3389           break;
3390         var currentEntry = this.getCurrentDirectoryEntry();
3391         var locationInfo = this.volumeManager_.getLocationInfo(currentEntry);
3392         // TODO(mtomasz): There may be a tiny race in here.
3393         if (locationInfo && !locationInfo.isRootEntry &&
3394             !locationInfo.isSpecialSearchRoot) {
3395           currentEntry.getParent(function(parentEntry) {
3396             this.directoryModel_.changeDirectoryEntry(parentEntry);
3397           }.bind(this), function() { /* Ignore errors. */});
3398         }
3399         break;
3400
3401       case '13':  // Enter => Change directory or perform default action.
3402         // TODO(dgozman): move directory action to dispatchSelectionAction.
3403         var selection = this.getSelection();
3404         if (selection.totalCount == 1 &&
3405             selection.entries[0].isDirectory &&
3406             !DialogType.isFolderDialog(this.dialogType)) {
3407           event.preventDefault();
3408           this.onDirectoryAction_(selection.entries[0]);
3409         } else if (this.dispatchSelectionAction_()) {
3410           event.preventDefault();
3411         }
3412         break;
3413     }
3414
3415     switch (event.keyIdentifier) {
3416       case 'Home':
3417       case 'End':
3418       case 'Up':
3419       case 'Down':
3420       case 'Left':
3421       case 'Right':
3422         // When navigating with keyboard we hide the distracting mouse hover
3423         // highlighting until the user moves the mouse again.
3424         this.setNoHover_(true);
3425         break;
3426     }
3427   };
3428
3429   /**
3430    * Suppress/restore hover highlighting in the list container.
3431    * @param {boolean} on True to temporarity hide hover state.
3432    * @private
3433    */
3434   FileManager.prototype.setNoHover_ = function(on) {
3435     if (on) {
3436       this.listContainer_.classList.add('nohover');
3437     } else {
3438       this.listContainer_.classList.remove('nohover');
3439     }
3440   };
3441
3442   /**
3443    * KeyPress event handler for the div#list-container element.
3444    * @param {Event} event Key event.
3445    * @private
3446    */
3447   FileManager.prototype.onListKeyPress_ = function(event) {
3448     if (event.srcElement.tagName == 'INPUT') {
3449       // Ignore keypress handler in the rename input box.
3450       return;
3451     }
3452
3453     if (event.ctrlKey || event.metaKey || event.altKey)
3454       return;
3455
3456     var now = new Date();
3457     var char = String.fromCharCode(event.charCode).toLowerCase();
3458     var text = now - this.textSearchState_.date > 1000 ? '' :
3459         this.textSearchState_.text;
3460     this.textSearchState_ = {text: text + char, date: now};
3461
3462     this.doTextSearch_();
3463   };
3464
3465   /**
3466    * Mousemove event handler for the div#list-container element.
3467    * @param {Event} event Mouse event.
3468    * @private
3469    */
3470   FileManager.prototype.onListMouseMove_ = function(event) {
3471     // The user grabbed the mouse, restore the hover highlighting.
3472     this.setNoHover_(false);
3473   };
3474
3475   /**
3476    * Performs a 'text search' - selects a first list entry with name
3477    * starting with entered text (case-insensitive).
3478    * @private
3479    */
3480   FileManager.prototype.doTextSearch_ = function() {
3481     var text = this.textSearchState_.text;
3482     if (!text)
3483       return;
3484
3485     var dm = this.directoryModel_.getFileList();
3486     for (var index = 0; index < dm.length; ++index) {
3487       var name = dm.item(index).name;
3488       if (name.substring(0, text.length).toLowerCase() == text) {
3489         this.currentList_.selectionModel.selectedIndexes = [index];
3490         return;
3491       }
3492     }
3493
3494     this.textSearchState_.text = '';
3495   };
3496
3497   /**
3498    * Handle a click of the cancel button.  Closes the window.
3499    * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3500    *
3501    * @param {Event} event The click event.
3502    * @private
3503    */
3504   FileManager.prototype.onCancel_ = function(event) {
3505     chrome.fileManagerPrivate.cancelDialog();
3506     window.close();
3507   };
3508
3509   /**
3510    * Tries to close this modal dialog with some files selected.
3511    * Performs preprocessing if needed (e.g. for Drive).
3512    * @param {Object} selection Contains urls, filterIndex and multiple fields.
3513    * @private
3514    */
3515   FileManager.prototype.selectFilesAndClose_ = function(selection) {
3516     var callSelectFilesApiAndClose = function(callback) {
3517       var onFileSelected = function() {
3518         callback();
3519         if (!chrome.runtime.lastError) {
3520           // Call next method on a timeout, as it's unsafe to
3521           // close a window from a callback.
3522           setTimeout(window.close.bind(window), 0);
3523         }
3524       };
3525       if (selection.multiple) {
3526         chrome.fileManagerPrivate.selectFiles(
3527             selection.urls,
3528             this.params_.shouldReturnLocalPath,
3529             onFileSelected);
3530       } else {
3531         chrome.fileManagerPrivate.selectFile(
3532             selection.urls[0],
3533             selection.filterIndex,
3534             this.dialogType != DialogType.SELECT_SAVEAS_FILE /* for opening */,
3535             this.params_.shouldReturnLocalPath,
3536             onFileSelected);
3537       }
3538     }.bind(this);
3539
3540     if (!this.isOnDrive() || this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3541       callSelectFilesApiAndClose(function() {});
3542       return;
3543     }
3544
3545     var shade = this.document_.createElement('div');
3546     shade.className = 'shade';
3547     var footer = this.dialogDom_.querySelector('.button-panel');
3548     var progress = footer.querySelector('.progress-track');
3549     progress.style.width = '0%';
3550     var cancelled = false;
3551
3552     var progressMap = {};
3553     var filesStarted = 0;
3554     var filesTotal = selection.urls.length;
3555     for (var index = 0; index < selection.urls.length; index++) {
3556       progressMap[selection.urls[index]] = -1;
3557     }
3558     var lastPercent = 0;
3559     var bytesTotal = 0;
3560     var bytesDone = 0;
3561
3562     var onFileTransfersUpdated = function(status) {
3563       if (!(status.fileUrl in progressMap))
3564         return;
3565       if (status.total == -1)
3566         return;
3567
3568       var old = progressMap[status.fileUrl];
3569       if (old == -1) {
3570         // -1 means we don't know file size yet.
3571         bytesTotal += status.total;
3572         filesStarted++;
3573         old = 0;
3574       }
3575       bytesDone += status.processed - old;
3576       progressMap[status.fileUrl] = status.processed;
3577
3578       var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
3579       // For files we don't have information about, assume the progress is zero.
3580       percent = percent * filesStarted / filesTotal * 100;
3581       // Do not decrease the progress. This may happen, if first downloaded
3582       // file is small, and the second one is large.
3583       lastPercent = Math.max(lastPercent, percent);
3584       progress.style.width = lastPercent + '%';
3585     }.bind(this);
3586
3587     var setup = function() {
3588       this.document_.querySelector('.dialog-container').appendChild(shade);
3589       setTimeout(function() { shade.setAttribute('fadein', 'fadein'); }, 100);
3590       footer.setAttribute('progress', 'progress');
3591       this.cancelButton_.removeEventListener('click', this.onCancelBound_);
3592       this.cancelButton_.addEventListener('click', onCancel);
3593       chrome.fileManagerPrivate.onFileTransfersUpdated.addListener(
3594           onFileTransfersUpdated);
3595     }.bind(this);
3596
3597     var cleanup = function() {
3598       shade.parentNode.removeChild(shade);
3599       footer.removeAttribute('progress');
3600       this.cancelButton_.removeEventListener('click', onCancel);
3601       this.cancelButton_.addEventListener('click', this.onCancelBound_);
3602       chrome.fileManagerPrivate.onFileTransfersUpdated.removeListener(
3603           onFileTransfersUpdated);
3604     }.bind(this);
3605
3606     var onCancel = function() {
3607       // According to API cancel may fail, but there is no proper UI to reflect
3608       // this. So, we just silently assume that everything is cancelled.
3609       chrome.fileManagerPrivate.cancelFileTransfers(
3610           selection.urls, function(response) {});
3611     }.bind(this);
3612
3613     var onProperties = function(properties) {
3614       for (var i = 0; i < properties.length; i++) {
3615         if (!properties[i] || properties[i].present) {
3616           // For files already in GCache, we don't get any transfer updates.
3617           filesTotal--;
3618         }
3619       }
3620       callSelectFilesApiAndClose(cleanup);
3621     }.bind(this);
3622
3623     setup();
3624
3625     // TODO(mtomasz): Use Entry instead of URLs, if possible.
3626     util.URLsToEntries(selection.urls, function(entries) {
3627       this.metadataCache_.get(entries, 'external', onProperties);
3628     }.bind(this));
3629   };
3630
3631   /**
3632    * Handle a click of the ok button.
3633    *
3634    * The ok button has different UI labels depending on the type of dialog, but
3635    * in code it's always referred to as 'ok'.
3636    *
3637    * @param {Event} event The click event.
3638    * @private
3639    */
3640   FileManager.prototype.onOk_ = function(event) {
3641     if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3642       // Save-as doesn't require a valid selection from the list, since
3643       // we're going to take the filename from the text input.
3644       var filename = this.filenameInput_.value;
3645       if (!filename)
3646         throw new Error('Missing filename!');
3647
3648       var directory = this.getCurrentDirectoryEntry();
3649       this.validateFileName_(directory, filename, function(isValid) {
3650         if (!isValid)
3651           return;
3652
3653         if (util.isFakeEntry(directory)) {
3654           // Can't save a file into a fake directory.
3655           return;
3656         }
3657
3658         var selectFileAndClose = function() {
3659           // TODO(mtomasz): Clean this up by avoiding constructing a URL
3660           //                via string concatenation.
3661           var currentDirUrl = directory.toURL();
3662           if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
3663             currentDirUrl += '/';
3664           this.selectFilesAndClose_({
3665             urls: [currentDirUrl + encodeURIComponent(filename)],
3666             multiple: false,
3667             filterIndex: this.getSelectedFilterIndex_(filename)
3668           });
3669         }.bind(this);
3670
3671         directory.getFile(
3672             filename, {create: false},
3673             function(entry) {
3674               // An existing file is found. Show confirmation dialog to
3675               // overwrite it. If the user select "OK" on the dialog, save it.
3676               this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
3677                                 selectFileAndClose);
3678             }.bind(this),
3679             function(error) {
3680               if (error.name == util.FileError.NOT_FOUND_ERR) {
3681                 // The file does not exist, so it should be ok to create a
3682                 // new file.
3683                 selectFileAndClose();
3684                 return;
3685               }
3686               if (error.name == util.FileError.TYPE_MISMATCH_ERR) {
3687                 // An directory is found.
3688                 // Do not allow to overwrite directory.
3689                 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
3690                 return;
3691               }
3692
3693               // Unexpected error.
3694               console.error('File save failed: ' + error.code);
3695             }.bind(this));
3696       }.bind(this));
3697       return;
3698     }
3699
3700     var files = [];
3701     var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
3702
3703     if (DialogType.isFolderDialog(this.dialogType) &&
3704         selectedIndexes.length == 0) {
3705       var url = this.getCurrentDirectoryEntry().toURL();
3706       var singleSelection = {
3707         urls: [url],
3708         multiple: false,
3709         filterIndex: this.getSelectedFilterIndex_()
3710       };
3711       this.selectFilesAndClose_(singleSelection);
3712       return;
3713     }
3714
3715     // All other dialog types require at least one selected list item.
3716     // The logic to control whether or not the ok button is enabled should
3717     // prevent us from ever getting here, but we sanity check to be sure.
3718     if (!selectedIndexes.length)
3719       throw new Error('Nothing selected!');
3720
3721     var dm = this.directoryModel_.getFileList();
3722     for (var i = 0; i < selectedIndexes.length; i++) {
3723       var entry = dm.item(selectedIndexes[i]);
3724       if (!entry) {
3725         console.error('Error locating selected file at index: ' + i);
3726         continue;
3727       }
3728
3729       files.push(entry.toURL());
3730     }
3731
3732     // Multi-file selection has no other restrictions.
3733     if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
3734       var multipleSelection = {
3735         urls: files,
3736         multiple: true
3737       };
3738       this.selectFilesAndClose_(multipleSelection);
3739       return;
3740     }
3741
3742     // Everything else must have exactly one.
3743     if (files.length > 1)
3744       throw new Error('Too many files selected!');
3745
3746     var selectedEntry = dm.item(selectedIndexes[0]);
3747
3748     if (DialogType.isFolderDialog(this.dialogType)) {
3749       if (!selectedEntry.isDirectory)
3750         throw new Error('Selected entry is not a folder!');
3751     } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
3752       if (!selectedEntry.isFile)
3753         throw new Error('Selected entry is not a file!');
3754     }
3755
3756     var singleSelection = {
3757       urls: [files[0]],
3758       multiple: false,
3759       filterIndex: this.getSelectedFilterIndex_()
3760     };
3761     this.selectFilesAndClose_(singleSelection);
3762   };
3763
3764   /**
3765    * Verifies the user entered name for file or folder to be created or
3766    * renamed to. See also util.validateFileName.
3767    *
3768    * @param {DirectoryEntry} parentEntry The URL of the parent directory entry.
3769    * @param {string} name New file or folder name.
3770    * @param {function} onDone Function to invoke when user closes the
3771    *    warning box or immediatelly if file name is correct. If the name was
3772    *    valid it is passed true, and false otherwise.
3773    * @private
3774    */
3775   FileManager.prototype.validateFileName_ = function(
3776       parentEntry, name, onDone) {
3777     var fileNameErrorPromise = util.validateFileName(
3778         parentEntry,
3779         name,
3780         this.fileFilter_.isFilterHiddenOn());
3781     fileNameErrorPromise.then(onDone.bind(null, true), function(message) {
3782       this.alert.show(message, onDone.bind(null, false));
3783     }.bind(this)).catch(function(error) {
3784       console.error(error.stack || error);
3785     });
3786   };
3787
3788   /**
3789    * Toggle whether mobile data is used for sync.
3790    */
3791   FileManager.prototype.toggleDriveSyncSettings = function() {
3792     // If checked, the sync is disabled.
3793     var nowCellularDisabled = this.syncButton.hasAttribute('checked');
3794     var changeInfo = {cellularDisabled: !nowCellularDisabled};
3795     chrome.fileManagerPrivate.setPreferences(changeInfo);
3796   };
3797
3798   /**
3799    * Toggle whether Google Docs files are shown.
3800    */
3801   FileManager.prototype.toggleDriveHostedSettings = function() {
3802     // If checked, showing drive hosted files is enabled.
3803     var nowHostedFilesEnabled = this.hostedButton.hasAttribute('checked');
3804     var nowHostedFilesDisabled = !nowHostedFilesEnabled;
3805     /*
3806     var changeInfo = {hostedFilesDisabled: !nowHostedFilesDisabled};
3807     */
3808     var changeInfo = {};
3809     changeInfo['hostedFilesDisabled'] = !nowHostedFilesDisabled;
3810     chrome.fileManagerPrivate.setPreferences(changeInfo);
3811   };
3812
3813   /**
3814    * Invoked when the search box is changed.
3815    *
3816    * @param {Event} event The changed event.
3817    * @private
3818    */
3819   FileManager.prototype.onSearchBoxUpdate_ = function(event) {
3820     var searchString = this.searchBox_.value;
3821
3822     if (this.isOnDrive()) {
3823       // When the search text is changed, finishes the search and showes back
3824       // the last directory by passing an empty string to
3825       // {@code DirectoryModel.search()}.
3826       if (this.directoryModel_.isSearching() &&
3827           this.lastSearchQuery_ != searchString) {
3828         this.doSearch('');
3829       }
3830
3831       // On drive, incremental search is not invoked since we have an auto-
3832       // complete suggestion instead.
3833       return;
3834     }
3835
3836     this.search_(searchString);
3837   };
3838
3839   /**
3840    * Handle the search clear button click.
3841    * @private
3842    */
3843   FileManager.prototype.onSearchClearButtonClick_ = function() {
3844     this.ui_.searchBox.clear();
3845     this.onSearchBoxUpdate_();
3846   };
3847
3848   /**
3849    * Search files and update the list with the search result.
3850    *
3851    * @param {string} searchString String to be searched with.
3852    * @private
3853    */
3854   FileManager.prototype.search_ = function(searchString) {
3855     var noResultsDiv = this.document_.getElementById('no-search-results');
3856
3857     var reportEmptySearchResults = function() {
3858       if (this.directoryModel_.getFileList().length === 0) {
3859         // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
3860         // hence we escapes |searchString| here.
3861         var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
3862                         util.htmlEscape(searchString));
3863         noResultsDiv.innerHTML = html;
3864         noResultsDiv.setAttribute('show', 'true');
3865       } else {
3866         noResultsDiv.removeAttribute('show');
3867       }
3868
3869       // If the current location is somewhere in Drive, all files in Drive can
3870       // be listed as search results regardless of current location.
3871       // In this case, showing current location is confusing, so use the Drive
3872       // root "My Drive" as the current location.
3873       var entry = this.getCurrentDirectoryEntry();
3874       var locationInfo = this.volumeManager_.getLocationInfo(entry);
3875       if (locationInfo && locationInfo.isDriveBased) {
3876         var rootEntry = locationInfo.volumeInfo.displayRoot;
3877         if (rootEntry)
3878           this.updateLocationLine_(rootEntry);
3879       }
3880     };
3881
3882     var hideNoResultsDiv = function() {
3883       noResultsDiv.removeAttribute('show');
3884       this.updateLocationLine_();
3885     };
3886
3887     this.doSearch(searchString,
3888                   reportEmptySearchResults.bind(this),
3889                   hideNoResultsDiv.bind(this));
3890   };
3891
3892   /**
3893    * Performs search and displays results.
3894    *
3895    * @param {string} searchString Query that will be searched for.
3896    * @param {function()=} opt_onSearchRescan Function that will be called when
3897    *     the search directory is rescanned (i.e. search results are displayed).
3898    * @param {function()=} opt_onClearSearch Function to be called when search
3899    *     state gets cleared.
3900    */
3901   FileManager.prototype.doSearch = function(
3902       searchString, opt_onSearchRescan, opt_onClearSearch) {
3903     var onSearchRescan = opt_onSearchRescan || function() {};
3904     var onClearSearch = opt_onClearSearch || function() {};
3905
3906     this.lastSearchQuery_ = searchString;
3907     this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
3908   };
3909
3910   /**
3911    * Requests autocomplete suggestions for files on Drive.
3912    * Once the suggestions are returned, the autocomplete popup will show up.
3913    *
3914    * @param {string} query The text to autocomplete from.
3915    * @private
3916    */
3917   FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
3918     query = query.trimLeft();
3919
3920     // Only Drive supports auto-compelete
3921     if (!this.isOnDrive())
3922       return;
3923
3924     // Remember the most recent query. If there is an other request in progress,
3925     // then it's result will be discarded and it will call a new request for
3926     // this query.
3927     this.lastAutocompleteQuery_ = query;
3928     if (this.autocompleteSuggestionsBusy_)
3929       return;
3930
3931     if (!query) {
3932       this.autocompleteList_.suggestions = [];
3933       return;
3934     }
3935
3936     var headerItem = {isHeaderItem: true, searchQuery: query};
3937     if (!this.autocompleteList_.dataModel ||
3938         this.autocompleteList_.dataModel.length == 0)
3939       this.autocompleteList_.suggestions = [headerItem];
3940     else
3941       // Updates only the head item to prevent a flickering on typing.
3942       this.autocompleteList_.dataModel.splice(0, 1, headerItem);
3943
3944     // The autocomplete list should be resized and repositioned here as the
3945     // search box is resized when it's focused.
3946     this.autocompleteList_.syncWidthAndPositionToInput();
3947
3948     this.autocompleteSuggestionsBusy_ = true;
3949
3950     var searchParams = {
3951       'query': query,
3952       'types': 'ALL',
3953       'maxResults': 4
3954     };
3955     chrome.fileManagerPrivate.searchDriveMetadata(
3956         searchParams,
3957         function(suggestions) {
3958           this.autocompleteSuggestionsBusy_ = false;
3959
3960           // Discard results for previous requests and fire a new search
3961           // for the most recent query.
3962           if (query != this.lastAutocompleteQuery_) {
3963             this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
3964             return;
3965           }
3966
3967           // Keeps the items in the suggestion list.
3968           this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
3969         }.bind(this));
3970   };
3971
3972   /**
3973    * Opens the currently selected suggestion item.
3974    * @private
3975    */
3976   FileManager.prototype.openAutocompleteSuggestion_ = function() {
3977     var selectedItem = this.autocompleteList_.selectedItem;
3978
3979     // If the entry is the search item or no entry is selected, just change to
3980     // the search result.
3981     if (!selectedItem || selectedItem.isHeaderItem) {
3982       var query = selectedItem ?
3983           selectedItem.searchQuery : this.searchBox_.value;
3984       this.search_(query);
3985       return;
3986     }
3987
3988     var entry = selectedItem.entry;
3989     // If the entry is a directory, just change the directory.
3990     if (entry.isDirectory) {
3991       this.onDirectoryAction_(entry);
3992       return;
3993     }
3994
3995     var entries = [entry];
3996     var self = this;
3997
3998     // To open a file, first get the mime type.
3999     this.metadataCache_.get(entries, 'external', function(props) {
4000       var mimeType = props[0].contentMimeType || '';
4001       var mimeTypes = [mimeType];
4002       var openIt = function() {
4003         if (self.dialogType == DialogType.FULL_PAGE) {
4004           var tasks = new FileTasks(self);
4005           tasks.init(entries, mimeTypes);
4006           tasks.executeDefault();
4007         } else {
4008           self.onOk_();
4009         }
4010       };
4011
4012       // Change the current directory to the directory that contains the
4013       // selected file. Note that this is necessary for an image or a video,
4014       // which should be opened in the gallery mode, as the gallery mode
4015       // requires the entry to be in the current directory model. For
4016       // consistency, the current directory is always changed regardless of
4017       // the file type.
4018       entry.getParent(function(parentEntry) {
4019         var onDirectoryChanged = function(event) {
4020           self.directoryModel_.removeEventListener('scan-completed',
4021                                                    onDirectoryChanged);
4022           self.directoryModel_.selectEntry(entry);
4023           openIt();
4024         };
4025         // changeDirectoryEntry() returns immediately. We should wait until the
4026         // directory scan is complete.
4027         self.directoryModel_.addEventListener('scan-completed',
4028                                               onDirectoryChanged);
4029         self.directoryModel_.changeDirectoryEntry(
4030             parentEntry,
4031             function() {
4032               // Remove the listner if the change directory failed.
4033               self.directoryModel_.removeEventListener('scan-completed',
4034                                                        onDirectoryChanged);
4035             });
4036       });
4037     });
4038   };
4039
4040   FileManager.prototype.decorateSplitter = function(splitterElement) {
4041     var self = this;
4042
4043     var Splitter = cr.ui.Splitter;
4044
4045     var customSplitter = cr.ui.define('div');
4046
4047     customSplitter.prototype = {
4048       __proto__: Splitter.prototype,
4049
4050       handleSplitterDragStart: function(e) {
4051         Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
4052         this.ownerDocument.documentElement.classList.add('col-resize');
4053       },
4054
4055       handleSplitterDragMove: function(deltaX) {
4056         Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
4057         self.onResize_();
4058       },
4059
4060       handleSplitterDragEnd: function(e) {
4061         Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
4062         this.ownerDocument.documentElement.classList.remove('col-resize');
4063       }
4064     };
4065
4066     customSplitter.decorate(splitterElement);
4067   };
4068
4069   /**
4070    * Updates default action menu item to match passed taskItem (icon,
4071    * label and action).
4072    *
4073    * @param {Object} defaultItem - taskItem to match.
4074    * @param {boolean} isMultiple - if multiple tasks available.
4075    */
4076   FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
4077                                                                 isMultiple) {
4078     if (defaultItem) {
4079       if (defaultItem.iconType) {
4080         this.defaultActionMenuItem_.style.backgroundImage = '';
4081         this.defaultActionMenuItem_.setAttribute('file-type-icon',
4082                                                  defaultItem.iconType);
4083       } else if (defaultItem.iconUrl) {
4084         this.defaultActionMenuItem_.style.backgroundImage =
4085             'url(' + defaultItem.iconUrl + ')';
4086       } else {
4087         this.defaultActionMenuItem_.style.backgroundImage = '';
4088       }
4089
4090       this.defaultActionMenuItem_.label = defaultItem.title;
4091       this.defaultActionMenuItem_.disabled = !!defaultItem.disabled;
4092       this.defaultActionMenuItem_.taskId = defaultItem.taskId;
4093     }
4094
4095     var defaultActionSeparator =
4096         this.dialogDom_.querySelector('#default-action-separator');
4097
4098     this.openWithCommand_.canExecuteChange();
4099     this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
4100     this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled;
4101
4102     this.defaultActionMenuItem_.hidden = !defaultItem;
4103     defaultActionSeparator.hidden = !defaultItem;
4104   };
4105
4106   /**
4107    * @return {FileSelection} Selection object.
4108    */
4109   FileManager.prototype.getSelection = function() {
4110     return this.selectionHandler_.selection;
4111   };
4112
4113   /**
4114    * @return {ArrayDataModel} File list.
4115    */
4116   FileManager.prototype.getFileList = function() {
4117     return this.directoryModel_.getFileList();
4118   };
4119
4120   /**
4121    * @return {cr.ui.List} Current list object.
4122    */
4123   FileManager.prototype.getCurrentList = function() {
4124     return this.currentList_;
4125   };
4126
4127   /**
4128    * Retrieve the preferences of the files.app. This method caches the result
4129    * and returns it unless opt_update is true.
4130    * @param {function(Object.<string, *>)} callback Callback to get the
4131    *     preference.
4132    * @param {boolean=} opt_update If is's true, don't use the cache and
4133    *     retrieve latest preference. Default is false.
4134    * @private
4135    */
4136   FileManager.prototype.getPreferences_ = function(callback, opt_update) {
4137     if (!opt_update && this.preferences_ !== null) {
4138       callback(this.preferences_);
4139       return;
4140     }
4141
4142     chrome.fileManagerPrivate.getPreferences(function(prefs) {
4143       this.preferences_ = prefs;
4144       callback(prefs);
4145     }.bind(this));
4146   };
4147 })();