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