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