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