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.
8 * FileManager constructor.
10 * FileManager objects encapsulate the functionality of the file selector
11 * dialogs, as well as the full screen file manager application (though the
12 * latter is not yet implemented).
16 function FileManager() {
17 // --------------------------------------------------------------------------
18 // Services FileManager depends on.
22 * @type {VolumeManager}
25 this.volumeManager_ = null;
29 * @type {MetadataCache}
32 this.metadataCache_ = null;
35 * File operation manager.
36 * @type {FileOperationManager}
39 this.fileOperationManager_ = null;
42 * File transfer controller.
43 * @type {FileTransferController}
46 this.fileTransferController_ = null;
53 this.fileFilter_ = null;
60 this.fileWatcher_ = null;
63 * Model of current directory.
64 * @type {DirectoryModel}
67 this.directoryModel_ = null;
70 * Model of folder shortcuts.
71 * @type {FolderShortcutsDataModel}
74 this.folderShortcutsModel_ = null;
77 * VolumeInfo of the current volume.
81 this.currentVolumeInfo_ = null;
84 * Handler for command events.
85 * @type {CommandHandler}
87 this.commandHandler = null;
90 * Handler for the change of file selection.
91 * @type {SelectionHandler}
94 this.selectionHandler_ = null;
96 // --------------------------------------------------------------------------
97 // Parameters determining the type of file manager.
100 * Dialog type of this window.
103 this.dialogType = DialogType.FULL_PAGE;
110 this.listType_ = null;
113 * List of acceptable file types for open dialog.
114 * @type {Array.<Object>}
117 this.fileTypes_ = [];
120 * Startup parameters for this application.
127 * Startup preference about the view.
131 this.viewOptions_ = {};
134 * The user preference.
138 this.preferences_ = null;
140 // --------------------------------------------------------------------------
144 * UI management class of file manager.
145 * @type {FileManagerUI}
152 * @type {PreviewPanel}
155 this.previewPanel_ = null;
158 * Progress center panel.
159 * @type {ProgressCenterPanel}
162 this.progressCenterPanel_ = null;
166 * @type {DirectoryTree}
169 this.directoryTree_ = null;
172 * Auto-complete list.
173 * @type {AutocompleteList}
176 this.autocompleteList_ = null;
179 * Banners in the file list.
180 * @type {FileListBannerController}
183 this.bannersController_ = null;
185 // --------------------------------------------------------------------------
190 * @type {ErrorDialog}
196 * @type {cr.ui.dialogs.AlertDialog}
202 * @type {cr.ui.dialogs.ConfirmDialog}
208 * @type {cr.ui.dialogs.PromptDialog}
214 * @type {ShareDialog}
217 this.shareDialog_ = null;
220 * Default task picker.
221 * @type {DefaultActionDialog}
223 this.defaultTaskPicker = null;
226 * Suggest apps dialog.
227 * @type {SuggestAppsDialog}
229 this.suggestAppsDialog = null;
231 // --------------------------------------------------------------------------
235 * Context menu for files.
236 * @type {HTMLMenuElement}
239 this.fileContextMenu_ = null;
242 * Context menu for volumes or shortcuts displayed on left pane.
243 * @type {HTMLMenuElement}
246 this.rootsContextMenu_ = null;
249 * Context menu for directory tree items.
250 * @type {HTMLMenuElement}
253 this.directoryTreeContextMenu_ = null;
256 * Context menu for texts.
257 * @type {HTMLMenuElement}
260 this.textContextMenu_ = null;
262 // --------------------------------------------------------------------------
270 this.backgroundPage_ = null;
273 * The root DOM element of this app.
274 * @type {HTMLBodyElement}
277 this.dialogDom_ = null;
280 * The document object of this app.
281 * @type {HTMLDocument}
284 this.document_ = null;
287 * The menu item to toggle "Do not use mobile data for sync".
288 * @type {HTMLMenuItemElement}
290 this.syncButton = null;
293 * The menu item to toggle "Show Google Docs files".
294 * @type {HTMLMenuItemElement}
296 this.hostedButton = null;
299 * The menu item for doing default action.
300 * @type {HTMLMenuItemElement}
303 this.defaultActionMenuItem_ = null;
306 * The button to open gear menu.
307 * @type {HTMLButtonElement}
310 this.gearButton_ = null;
314 * @type {HTMLButtonElement}
317 this.okButton_ = null;
321 * @type {HTMLButtonElement}
324 this.cancelButton_ = null;
327 * The combo button to specify the task.
328 * @type {HTMLButtonElement}
331 this.taskItems_ = null;
334 * The input element to rename entry.
335 * @type {HTMLInputElement}
338 this.renameInput_ = null;
341 * The input element to specify file name.
342 * @type {HTMLInputElement}
345 this.filenameInput_ = null;
366 this.currentList_ = null;
369 * Spinner on file list which is shown while loading.
370 * @type {HTMLDivElement}
373 this.spinner_ = null;
376 * The container element of the dialog.
377 * @type {HTMLDivElement}
380 this.dialogContainer_ = null;
383 * The container element of the file list.
384 * @type {HTMLDivElement}
387 this.listContainer_ = null;
390 * The input element in the search box.
391 * @type {HTMLInputElement}
394 this.searchBox_ = null;
397 * The file type selector.
398 * @type {HTMLSelectElement}
401 this.fileTypeSelector_ = null;
404 * Open-with command in the context menu.
405 * @type {cr.ui.Command}
408 this.openWithCommand_ = null;
410 // --------------------------------------------------------------------------
414 * Bound function for onCopyProgress_.
415 * @type {this:FileManager, function(Event)}
418 this.onCopyProgressBound_ = null;
421 * Bound function for onEntriesChanged_.
422 * @type {this:FileManager, function(Event)}
425 this.onEntriesChangedBound_ = null;
428 * Bound function for onCancel_.
429 * @type {this:FileManager, function(Event)}
432 this.onCancelBound_ = null;
434 // --------------------------------------------------------------------------
438 * Whether a scan is in progress.
442 this.scanInProgress_ = false;
445 * Whether a scan is updated at least once. If true, spinner should disappear.
449 this.scanUpdatedAtLeastOnceOrCompleted_ = false;
452 * Timer ID to delay UI refresh after a scan is completed.
456 this.scanCompletedTimer_ = 0;
459 * Timer ID to delay UI refresh after a scan is updated.
463 this.scanUpdatedTimer_ = 0;
466 * Timer ID to delay showing spinner after a scan starts.
470 this.showSpinnerTimeout_ = 0;
472 // --------------------------------------------------------------------------
476 * The last search query.
480 this.lastSearchQuery_ = '';
483 * The last auto-complete query.
487 this.lastAutocompleteQuery_ = '';
490 * Whether auto-complete suggestion is busy to respond previous request.
494 this.autocompleteSuggestionsBusy_ = false;
497 * State of text-search, which is triggerd by keyboard input on file list.
501 this.textSearchState_ = {text: '', date: new Date()};
503 // --------------------------------------------------------------------------
504 // Miscellaneous FileManager's states.
507 * Queue for ordering FileManager's initialization process.
508 * @type {AsyncUtil.Group}
511 this.initializeQueue_ = new AsyncUtil.Group();
514 * True while a user is pressing <Tab>.
515 * This is used for identifying the trigger causing the filelist to
520 this.pressingTab_ = false;
523 * True while a user is pressing <Ctrl>.
525 * TODO(fukino): This key is used only for controlling gear menu, so it
526 * should be moved to GearMenu class. crbug.com/366032.
531 this.pressingCtrl_ = false;
534 * True if shown gear menu is in secret mode.
536 * TODO(fukino): The state of gear menu should be moved to GearMenu class.
542 this.isSecretGearMenuShown_ = false;
545 * The last clicked item in the file list.
546 * @type {HTMLLIElement}
549 this.lastClickedItem_ = null;
552 * Count of the SourceNotFound error.
556 this.sourceNotFoundErrorCount_ = 0;
559 * Whether the app should be closed on unmount.
563 this.closeOnUnmount_ = false;
566 * The key for storing startup preference.
570 this.startupPrefName_ = '';
573 * URL of directory which should be initial current directory.
577 this.initCurrentDirectoryURL_ = '';
580 * URL of entry which should be initially selected.
584 this.initSelectionURL_ = '';
587 * The name of target entry (not URL).
591 this.initTargetName_ = '';
594 * Data model which is used as a placefolder in inactive file list.
595 * @type {cr.ui.ArrayDataModel}
598 this.emptyDataModel_ = null;
601 * Selection model which is used as a placefolder in inactive file list.
602 * @type {cr.ui.ListSelectionModel}
605 this.emptySelectionModel_ = null;
607 // Object.seal() has big performance/memory overhead for now, so we use
608 // Object.preventExtensions() here. crbug.com/412239.
609 Object.preventExtensions(this);
612 FileManager.prototype = {
613 __proto__: cr.EventTarget.prototype,
614 get directoryModel() {
615 return this.directoryModel_;
617 get directoryTree() {
618 return this.directoryTree_;
621 return this.document_;
623 get fileTransferController() {
624 return this.fileTransferController_;
626 get fileOperationManager() {
627 return this.fileOperationManager_;
629 get backgroundPage() {
630 return this.backgroundPage_;
632 get volumeManager() {
633 return this.volumeManager_;
641 * List of dialog types.
643 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
644 * FULL_PAGE which is specific to this code.
650 SELECT_FOLDER: 'folder',
651 SELECT_UPLOAD_FOLDER: 'upload-folder',
652 SELECT_SAVEAS_FILE: 'saveas-file',
653 SELECT_OPEN_FILE: 'open-file',
654 SELECT_OPEN_MULTI_FILE: 'open-multi-file',
655 FULL_PAGE: 'full-page'
659 * @param {string} type Dialog type.
660 * @return {boolean} Whether the type is modal.
662 DialogType.isModal = function(type) {
663 return type == DialogType.SELECT_FOLDER ||
664 type == DialogType.SELECT_UPLOAD_FOLDER ||
665 type == DialogType.SELECT_SAVEAS_FILE ||
666 type == DialogType.SELECT_OPEN_FILE ||
667 type == DialogType.SELECT_OPEN_MULTI_FILE;
671 * @param {string} type Dialog type.
672 * @return {boolean} Whether the type is open dialog.
674 DialogType.isOpenDialog = function(type) {
675 return type == DialogType.SELECT_OPEN_FILE ||
676 type == DialogType.SELECT_OPEN_MULTI_FILE ||
677 type == DialogType.SELECT_FOLDER ||
678 type == DialogType.SELECT_UPLOAD_FOLDER;
682 * @param {string} type Dialog type.
683 * @return {boolean} Whether the type is open dialog for file(s).
685 DialogType.isOpenFileDialog = function(type) {
686 return type == DialogType.SELECT_OPEN_FILE ||
687 type == DialogType.SELECT_OPEN_MULTI_FILE;
691 * @param {string} type Dialog type.
692 * @return {boolean} Whether the type is folder selection dialog.
694 DialogType.isFolderDialog = function(type) {
695 return type == DialogType.SELECT_FOLDER ||
696 type == DialogType.SELECT_UPLOAD_FOLDER;
699 Object.freeze(DialogType);
702 * Bottom margin of the list and tree for transparent preview panel.
705 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
707 // Anonymous "namespace".
710 // Private variables and helper functions.
713 * Number of milliseconds in a day.
715 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
718 * Some UI elements react on a single click and standard double click handling
719 * leads to confusing results. We ignore a second click if it comes soon
722 var DOUBLE_CLICK_TIMEOUT = 200;
725 * Updates the element to display the information about remaining space for
728 * @param {!Object<string, number>} sizeStatsResult Map containing remaining
730 * @param {!Element} spaceInnerBar Block element for a percentage bar
731 * representing the remaining space.
732 * @param {!Element} spaceInfoLabel Inline element to contain the message.
733 * @param {!Element} spaceOuterBar Block element around the percentage bar.
735 var updateSpaceInfo = function(
736 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
737 spaceInnerBar.removeAttribute('pending');
738 if (sizeStatsResult) {
739 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
740 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
743 sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
744 spaceInnerBar.style.width =
745 (100 * usedSpace / sizeStatsResult.totalSize) + '%';
747 spaceOuterBar.hidden = false;
749 spaceOuterBar.hidden = true;
750 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
756 FileManager.ListType = {
761 FileManager.prototype.initPreferences_ = function(callback) {
762 var group = new AsyncUtil.Group();
764 // DRIVE preferences should be initialized before creating DirectoryModel
765 // to rebuild the roots list.
766 group.add(this.getPreferences_.bind(this));
768 // Get startup preferences.
769 group.add(function(done) {
770 chrome.storage.local.get(this.startupPrefName_, function(values) {
771 var value = values[this.startupPrefName_];
776 // Load the global default options.
778 this.viewOptions_ = JSON.parse(value);
780 // Override with window-specific options.
781 if (window.appState && window.appState.viewOptions) {
782 for (var key in window.appState.viewOptions) {
783 if (window.appState.viewOptions.hasOwnProperty(key))
784 this.viewOptions_[key] = window.appState.viewOptions[key];
795 * One time initialization for the file system and related things.
797 * @param {function()} callback Completion callback.
800 FileManager.prototype.initFileSystemUI_ = function(callback) {
801 this.table_.startBatchUpdates();
802 this.grid_.startBatchUpdates();
804 this.initFileList_();
805 this.setupCurrentDirectory_();
807 // PyAuto tests monitor this state by polling this variable
808 this.__defineGetter__('workerInitialized_', function() {
809 return this.metadataCache_.isInitialized();
812 this.initDateTimeFormatters_();
816 // Get the 'allowRedeemOffers' preference before launching
817 // FileListBannerController.
818 this.getPreferences_(function(pref) {
819 /** @type {boolean} */
820 var showOffers = pref['allowRedeemOffers'];
821 self.bannersController_ = new FileListBannerController(
822 self.directoryModel_, self.volumeManager_, self.document_,
824 self.bannersController_.addEventListener('relayout',
825 self.onResize_.bind(self));
828 var dm = this.directoryModel_;
829 dm.addEventListener('directory-changed',
830 this.onDirectoryChanged_.bind(this));
832 var listBeingUpdated = null;
833 dm.addEventListener('begin-update-files', function() {
834 self.currentList_.startBatchUpdates();
835 // Remember the list which was used when updating files started, so
836 // endBatchUpdates() is called on the same list.
837 listBeingUpdated = self.currentList_;
839 dm.addEventListener('end-update-files', function() {
840 self.restoreItemBeingRenamed_();
841 listBeingUpdated.endBatchUpdates();
842 listBeingUpdated = null;
845 dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
846 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
847 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
848 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
849 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
850 dm.addEventListener('rescan-completed',
851 this.onRescanCompleted_.bind(this));
853 this.directoryTree_.addEventListener('change', function() {
854 this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
857 var stateChangeHandler =
858 this.onPreferencesChanged_.bind(this);
859 chrome.fileManagerPrivate.onPreferencesChanged.addListener(
861 stateChangeHandler();
863 var driveConnectionChangedHandler =
864 this.onDriveConnectionChanged_.bind(this);
865 this.volumeManager_.addEventListener('drive-connection-changed',
866 driveConnectionChangedHandler);
867 driveConnectionChangedHandler();
869 // Set the initial focus.
871 // Set it as a fallback when there is no focus.
872 this.document_.addEventListener('focusout', function(e) {
873 setTimeout(function() {
874 // When there is no focus, the active element is the <body>.
875 if (this.document_.activeElement == this.document_.body)
880 this.initDataTransferOperations_();
882 this.initContextMenus_();
883 this.initCommands_();
885 this.updateFileTypeFilter_();
887 this.selectionHandler_.onFileSelectionChanged();
889 this.table_.endBatchUpdates();
890 this.grid_.endBatchUpdates();
896 * If |item| in the directory tree is behind the preview panel, scrolls up the
897 * parent view and make the item visible. This should be called when:
898 * - the selected item is changed in the directory tree.
899 * - the visibility of the the preview panel is changed.
903 FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ =
905 var selectedSubTree = this.directoryTree_.selectedItem;
906 if (!selectedSubTree)
908 var item = selectedSubTree.rowElement;
909 var parentView = this.directoryTree_;
911 var itemRect = item.getBoundingClientRect();
915 var listRect = parentView.getBoundingClientRect();
919 var previewPanel = this.dialogDom_.querySelector('.preview-panel');
920 var previewPanelRect = previewPanel.getBoundingClientRect();
921 var panelHeight = previewPanelRect ? previewPanelRect.height : 0;
923 var itemBottom = itemRect.bottom;
924 var listBottom = listRect.bottom - panelHeight;
926 if (itemBottom > listBottom) {
927 var scrollOffset = itemBottom - listBottom;
928 parentView.scrollTop += scrollOffset;
935 FileManager.prototype.initDateTimeFormatters_ = function() {
936 var use12hourClock = !this.preferences_['use24hourClock'];
937 this.table_.setDateTimeFormat(use12hourClock);
943 FileManager.prototype.initDataTransferOperations_ = function() {
944 this.fileOperationManager_ =
945 this.backgroundPage_.background.fileOperationManager;
947 // CopyManager are required for 'Delete' operation in
948 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
949 if (this.dialogType != DialogType.FULL_PAGE) return;
951 // TODO(hidehiko): Extract FileOperationManager related code from
952 // FileManager to simplify it.
953 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
954 this.fileOperationManager_.addEventListener(
955 'copy-progress', this.onCopyProgressBound_);
957 this.onEntriesChangedBound_ = this.onEntriesChanged_.bind(this);
958 this.fileOperationManager_.addEventListener(
959 'entries-changed', this.onEntriesChangedBound_);
961 var controller = this.fileTransferController_ =
962 new FileTransferController(
964 this.fileOperationManager_,
966 this.directoryModel_,
968 this.ui_.multiProfileShareDialog,
969 this.backgroundPage_.background.progressCenter);
970 controller.attachDragSource(this.table_.list);
971 controller.attachFileListDropTarget(this.table_.list);
972 controller.attachDragSource(this.grid_);
973 controller.attachFileListDropTarget(this.grid_);
974 controller.attachTreeDropTarget(this.directoryTree_);
975 controller.attachCopyPasteHandlers();
976 controller.addEventListener('selection-copied',
977 this.blinkSelection.bind(this));
978 controller.addEventListener('selection-cut',
979 this.blinkSelection.bind(this));
980 controller.addEventListener('source-not-found',
981 this.onSourceNotFound_.bind(this));
985 * Handles an error that the source entry of file operation is not found.
988 FileManager.prototype.onSourceNotFound_ = function(event) {
989 var item = new ProgressCenterItem();
990 item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_;
991 if (event.progressType === ProgressItemType.COPY)
992 item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', event.fileName);
993 else if (event.progressType === ProgressItemType.MOVE)
994 item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', event.fileName);
995 item.state = ProgressItemState.ERROR;
996 this.backgroundPage_.background.progressCenter.updateItem(item);
997 this.sourceNotFoundErrorCount_++;
1001 * One-time initialization of context menus.
1004 FileManager.prototype.initContextMenus_ = function() {
1005 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
1006 cr.ui.Menu.decorate(this.fileContextMenu_);
1008 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
1009 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
1010 this.fileContextMenu_);
1011 cr.ui.contextMenuHandler.setContextMenu(
1012 this.document_.querySelector('.drive-welcome.page'),
1013 this.fileContextMenu_);
1015 this.rootsContextMenu_ =
1016 this.dialogDom_.querySelector('#roots-context-menu');
1017 cr.ui.Menu.decorate(this.rootsContextMenu_);
1018 this.directoryTree_.contextMenuForRootItems = this.rootsContextMenu_;
1020 this.directoryTreeContextMenu_ =
1021 this.dialogDom_.querySelector('#directory-tree-context-menu');
1022 cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
1023 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
1025 this.textContextMenu_ =
1026 this.dialogDom_.querySelector('#text-context-menu');
1027 cr.ui.Menu.decorate(this.textContextMenu_);
1029 this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
1030 this.gearButton_.addEventListener('menushow',
1031 this.onShowGearMenu_.bind(this));
1033 this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
1035 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
1037 this.syncButton.checkable = true;
1038 this.hostedButton.checkable = true;
1040 if (util.runningInBrowser()) {
1041 // Suppresses the default context menu.
1042 this.dialogDom_.addEventListener('contextmenu', function(e) {
1044 e.stopPropagation();
1049 FileManager.prototype.onShowGearMenu_ = function() {
1050 this.refreshRemainingSpace_(false); /* Without loading caption. */
1052 // If the menu is opened while CTRL key pressed, secret menu itemscan be
1054 this.isSecretGearMenuShown_ = this.pressingCtrl_;
1056 // Update view of drive-related settings.
1057 this.commandHandler.updateAvailability();
1058 this.document_.getElementById('drive-separator').hidden =
1059 !this.shouldShowDriveSettings();
1061 // Force to update the gear menu position.
1062 // TODO(hirono): Remove the workaround for the crbug.com/374093 after fixing
1064 var gearMenu = this.document_.querySelector('#gear-menu');
1065 gearMenu.style.left = '';
1066 gearMenu.style.right = '';
1067 gearMenu.style.top = '';
1068 gearMenu.style.bottom = '';
1072 * One-time initialization of commands.
1075 FileManager.prototype.initCommands_ = function() {
1076 this.commandHandler = new CommandHandler(this);
1078 // TODO(hirono): Move the following block to the UI part.
1079 var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
1080 for (var j = 0; j < commandButtons.length; j++)
1081 CommandButton.decorate(commandButtons[j]);
1083 var inputs = this.dialogDom_.querySelectorAll(
1084 'input[type=text], input[type=search], textarea');
1085 for (var i = 0; i < inputs.length; i++) {
1086 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
1087 this.registerInputCommands_(inputs[i]);
1090 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
1091 this.textContextMenu_);
1092 this.registerInputCommands_(this.renameInput_);
1093 this.document_.addEventListener('command',
1094 this.setNoHover_.bind(this, true));
1098 * Registers cut, copy, paste and delete commands on input element.
1100 * @param {Node} node Text input element to register on.
1103 FileManager.prototype.registerInputCommands_ = function(node) {
1104 CommandUtil.forceDefaultHandler(node, 'cut');
1105 CommandUtil.forceDefaultHandler(node, 'copy');
1106 CommandUtil.forceDefaultHandler(node, 'paste');
1107 CommandUtil.forceDefaultHandler(node, 'delete');
1108 node.addEventListener('keydown', function(e) {
1109 var key = util.getKeyModifiers(e) + e.keyCode;
1110 if (key === '190' /* '/' */ || key === '191' /* '.' */) {
1111 // If this key event is propagated, this is handled search command,
1112 // which calls 'preventDefault' method.
1113 e.stopPropagation();
1119 * Entry point of the initialization.
1120 * This method is called from main.js.
1122 FileManager.prototype.initializeCore = function() {
1123 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
1124 this.initializeQueue_.add(this.initBackgroundPage_.bind(this),
1125 [], 'initBackgroundPage');
1126 this.initializeQueue_.add(this.initPreferences_.bind(this),
1127 ['initGeneral'], 'initPreferences');
1128 this.initializeQueue_.add(this.initVolumeManager_.bind(this),
1129 ['initGeneral', 'initBackgroundPage'],
1130 'initVolumeManager');
1132 this.initializeQueue_.run();
1133 window.addEventListener('pagehide', this.onUnload_.bind(this));
1136 FileManager.prototype.initializeUI = function(dialogDom, callback) {
1137 this.dialogDom_ = dialogDom;
1138 this.document_ = this.dialogDom_.ownerDocument;
1140 this.initializeQueue_.add(
1141 this.initEssentialUI_.bind(this),
1142 ['initGeneral', 'initBackgroundPage'],
1144 this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
1145 ['initEssentialUI'], 'initAdditionalUI');
1146 this.initializeQueue_.add(
1147 this.initFileSystemUI_.bind(this),
1148 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI');
1150 // Run again just in case if all pending closures have completed and the
1151 // queue has stopped and monitor the completion.
1152 this.initializeQueue_.run(callback);
1156 * Initializes general purpose basic things, which are used by other
1157 * initializing methods.
1159 * @param {function()} callback Completion callback.
1162 FileManager.prototype.initGeneral_ = function(callback) {
1163 // Initialize the application state.
1164 // TODO(mtomasz): Unify window.appState with location.search format.
1165 if (window.appState) {
1166 this.params_ = window.appState.params || {};
1167 this.initCurrentDirectoryURL_ = window.appState.currentDirectoryURL;
1168 this.initSelectionURL_ = window.appState.selectionURL;
1169 this.initTargetName_ = window.appState.targetName;
1171 // Used by the select dialog only.
1172 this.params_ = location.search ?
1173 JSON.parse(decodeURIComponent(location.search.substr(1))) :
1175 this.initCurrentDirectoryURL_ = this.params_.currentDirectoryURL;
1176 this.initSelectionURL_ = this.params_.selectionURL;
1177 this.initTargetName_ = this.params_.targetName;
1180 // Initialize the member variables that depend this.params_.
1181 this.dialogType = this.params_.type || DialogType.FULL_PAGE;
1182 this.startupPrefName_ = 'file-manager-' + this.dialogType;
1183 this.fileTypes_ = this.params_.typeList || [];
1189 * Initialize the background page.
1190 * @param {function()} callback Completion callback.
1193 FileManager.prototype.initBackgroundPage_ = function(callback) {
1194 chrome.runtime.getBackgroundPage(function(backgroundPage) {
1195 this.backgroundPage_ = backgroundPage;
1196 this.backgroundPage_.background.ready(function() {
1197 loadTimeData.data = this.backgroundPage_.background.stringData;
1198 if (util.runningInBrowser())
1199 this.backgroundPage_.registerDialog(window);
1206 * Initializes the VolumeManager instance.
1207 * @param {function()} callback Completion callback.
1210 FileManager.prototype.initVolumeManager_ = function(callback) {
1211 // Auto resolving to local path does not work for folders (e.g., dialog for
1212 // loading unpacked extensions).
1213 var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type);
1215 // If this condition is false, VolumeManagerWrapper hides all drive
1216 // related event and data, even if Drive is enabled on preference.
1217 // In other words, even if Drive is disabled on preference but Files.app
1218 // should show Drive when it is re-enabled, then the value should be set to
1220 // Note that the Drive enabling preference change is listened by
1221 // DriveIntegrationService, so here we don't need to take care about it.
1223 !noLocalPathResolution || !this.params_.shouldReturnLocalPath;
1224 this.volumeManager_ = new VolumeManagerWrapper(
1225 driveEnabled, this.backgroundPage_);
1230 * One time initialization of the Files.app's essential UI elements. These
1231 * elements will be shown to the user. Only visible elements should be
1232 * initialized here. Any heavy operation should be avoided. Files.app's
1233 * window is shown at the end of this routine.
1235 * @param {function()} callback Completion callback.
1238 FileManager.prototype.initEssentialUI_ = function(callback) {
1239 // Record stats of dialog types. New values must NOT be inserted into the
1240 // array enumerating the types. It must be in sync with
1241 // FileDialogType enum in tools/metrics/histograms/histogram.xml.
1242 metrics.recordEnum('Create', this.dialogType,
1243 [DialogType.SELECT_FOLDER,
1244 DialogType.SELECT_UPLOAD_FOLDER,
1245 DialogType.SELECT_SAVEAS_FILE,
1246 DialogType.SELECT_OPEN_FILE,
1247 DialogType.SELECT_OPEN_MULTI_FILE,
1248 DialogType.FULL_PAGE]);
1250 // Create the metadata cache.
1251 this.metadataCache_ = MetadataCache.createFull(this.volumeManager_);
1253 // Create the root view of FileManager.
1254 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType);
1255 this.fileTypeSelector_ = this.ui_.fileTypeSelector;
1256 this.okButton_ = this.ui_.okButton;
1257 this.cancelButton_ = this.ui_.cancelButton;
1259 // Show the window as soon as the UI pre-initialization is done.
1260 if (this.dialogType == DialogType.FULL_PAGE && !util.runningInBrowser()) {
1261 chrome.app.window.current().show();
1262 setTimeout(callback, 100); // Wait until the animation is finished.
1269 * One-time initialization of dialogs.
1272 FileManager.prototype.initDialogs_ = function() {
1273 // Initialize the dialog.
1274 this.ui_.initDialogs();
1275 FileManagerDialogBase.setFileManager(this);
1277 // Obtains the dialog instances from FileManagerUI.
1278 // TODO(hirono): Remove the properties from the FileManager class.
1279 this.error = this.ui_.errorDialog;
1280 this.alert = this.ui_.alertDialog;
1281 this.confirm = this.ui_.confirmDialog;
1282 this.prompt = this.ui_.promptDialog;
1283 this.shareDialog_ = this.ui_.shareDialog;
1284 this.defaultTaskPicker = this.ui_.defaultTaskPicker;
1285 this.suggestAppsDialog = this.ui_.suggestAppsDialog;
1289 * One-time initialization of various DOM nodes. Loads the additional DOM
1290 * elements visible to the user. Initialize here elements, which are expensive
1291 * or hidden in the beginning.
1293 * @param {function()} callback Completion callback.
1296 FileManager.prototype.initAdditionalUI_ = function(callback) {
1297 this.initDialogs_();
1298 this.ui_.initAdditionalUI();
1300 this.dialogDom_.addEventListener('drop', function(e) {
1301 // Prevent opening an URL by dropping it onto the page.
1305 this.dialogDom_.addEventListener('click',
1306 this.onExternalLinkClick_.bind(this));
1307 // Cache nodes we'll be manipulating.
1308 var dom = this.dialogDom_;
1310 this.filenameInput_ = dom.querySelector('#filename-input-box input');
1311 this.taskItems_ = dom.querySelector('#tasks');
1313 this.table_ = dom.querySelector('.detail-table');
1314 this.grid_ = dom.querySelector('.thumbnail-grid');
1315 this.spinner_ = dom.querySelector('#list-container > .spinner-layer');
1316 this.showSpinner_(true);
1318 var fullPage = this.dialogType == DialogType.FULL_PAGE;
1320 this.table_, this.metadataCache_, this.volumeManager_, fullPage);
1321 FileGrid.decorate(this.grid_, this.metadataCache_, this.volumeManager_);
1323 this.ui_.locationBreadcrumbs = new BreadcrumbsController(
1324 dom.querySelector('#location-breadcrumbs'),
1325 this.metadataCache_,
1326 this.volumeManager_);
1327 this.ui_.locationBreadcrumbs.addEventListener(
1328 'pathclick', this.onBreadcrumbClick_.bind(this));
1330 this.previewPanel_ = new PreviewPanel(
1331 dom.querySelector('.preview-panel'),
1332 DialogType.isOpenDialog(this.dialogType) ?
1333 PreviewPanel.VisibilityType.ALWAYS_VISIBLE :
1334 PreviewPanel.VisibilityType.AUTO,
1335 this.metadataCache_,
1336 this.volumeManager_);
1337 this.previewPanel_.addEventListener(
1338 PreviewPanel.Event.VISIBILITY_CHANGE,
1339 this.onPreviewPanelVisibilityChange_.bind(this));
1340 this.previewPanel_.initialize();
1342 // Initialize progress center panel.
1343 this.progressCenterPanel_ = new ProgressCenterPanel(
1344 dom.querySelector('#progress-center'));
1345 this.backgroundPage_.background.progressCenter.addPanel(
1346 this.progressCenterPanel_);
1348 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
1349 this.document_.addEventListener('keyup', this.onKeyUp_.bind(this));
1351 this.renameInput_ = this.document_.createElement('input');
1352 this.renameInput_.className = 'rename entry-name';
1354 this.renameInput_.addEventListener(
1355 'keydown', this.onRenameInputKeyDown_.bind(this));
1356 this.renameInput_.addEventListener(
1357 'blur', this.onRenameInputBlur_.bind(this));
1359 // TODO(hirono): Rename the handler after creating the DialogFooter class.
1360 this.filenameInput_.addEventListener(
1361 'input', this.onFilenameInputInput_.bind(this));
1362 this.filenameInput_.addEventListener(
1363 'keydown', this.onFilenameInputKeyDown_.bind(this));
1364 this.filenameInput_.addEventListener(
1365 'focus', this.onFilenameInputFocus_.bind(this));
1367 this.listContainer_ = this.dialogDom_.querySelector('#list-container');
1368 this.listContainer_.addEventListener(
1369 'keydown', this.onListKeyDown_.bind(this));
1370 this.listContainer_.addEventListener(
1371 'keypress', this.onListKeyPress_.bind(this));
1372 this.listContainer_.addEventListener(
1373 'mousemove', this.onListMouseMove_.bind(this));
1375 this.okButton_.addEventListener('click', this.onOk_.bind(this));
1376 this.onCancelBound_ = this.onCancel_.bind(this);
1377 this.cancelButton_.addEventListener('click', this.onCancelBound_);
1379 this.decorateSplitter(
1380 this.dialogDom_.querySelector('#navigation-list-splitter'));
1382 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
1384 this.syncButton = this.dialogDom_.querySelector(
1385 '#gear-menu-drive-sync-settings');
1386 this.hostedButton = this.dialogDom_.querySelector(
1387 '#gear-menu-drive-hosted-settings');
1389 this.ui_.toggleViewButton.addEventListener('click',
1390 this.onToggleViewButtonClick_.bind(this));
1392 cr.ui.ComboButton.decorate(this.taskItems_);
1393 this.taskItems_.showMenu = function(shouldSetFocus) {
1394 // Prevent the empty menu from opening.
1395 if (!this.menu.length)
1397 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus);
1399 this.taskItems_.addEventListener('select',
1400 this.onTaskItemClicked_.bind(this));
1402 this.dialogDom_.ownerDocument.defaultView.addEventListener(
1403 'resize', this.onResize_.bind(this));
1405 this.searchBox_ = this.ui_.searchBox.inputElement;
1406 this.searchBox_.addEventListener(
1407 'input', this.onSearchBoxUpdate_.bind(this));
1408 this.ui_.searchBox.clearButton.addEventListener(
1409 'click', this.onSearchClearButtonClick_.bind(this));
1411 this.autocompleteList_ = this.ui_.searchBox.autocompleteList;
1412 this.autocompleteList_.requestSuggestions =
1413 this.requestAutocompleteSuggestions_.bind(this);
1415 // Instead, open the suggested item when Enter key is pressed or
1417 this.autocompleteList_.handleEnterKeydown = function(event) {
1418 this.openAutocompleteSuggestion_();
1419 this.lastAutocompleteQuery_ = '';
1420 this.autocompleteList_.suggestions = [];
1422 this.autocompleteList_.addEventListener('mousedown', function(event) {
1423 this.openAutocompleteSuggestion_();
1424 this.lastAutocompleteQuery_ = '';
1425 this.autocompleteList_.suggestions = [];
1428 this.defaultActionMenuItem_ =
1429 this.dialogDom_.querySelector('#default-action');
1431 this.openWithCommand_ =
1432 this.dialogDom_.querySelector('#open-with');
1434 this.defaultActionMenuItem_.addEventListener('activate',
1435 this.dispatchSelectionAction_.bind(this));
1437 this.initFileTypeFilter_();
1439 util.addIsFocusedMethod();
1441 // Populate the static localized strings.
1442 i18nTemplate.process(this.document_, loadTimeData);
1444 // Arrange the file list.
1445 this.table_.normalizeColumns();
1446 this.table_.redraw();
1452 * @param {Event} event Click event.
1455 FileManager.prototype.onBreadcrumbClick_ = function(event) {
1456 this.directoryModel_.changeDirectoryEntry(event.entry);
1460 * Constructs table and grid (heavy operation).
1463 FileManager.prototype.initFileList_ = function() {
1464 // Always sharing the data model between the detail/thumb views confuses
1465 // them. Instead we maintain this bogus data model, and hook it up to the
1466 // view that is not in use.
1467 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
1468 this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
1470 var singleSelection =
1471 this.dialogType == DialogType.SELECT_OPEN_FILE ||
1472 this.dialogType == DialogType.SELECT_FOLDER ||
1473 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
1474 this.dialogType == DialogType.SELECT_SAVEAS_FILE;
1476 this.fileFilter_ = new FileFilter(
1477 this.metadataCache_,
1478 false /* Don't show dot files and *.crdownload by default. */);
1480 this.fileWatcher_ = new FileWatcher(this.metadataCache_);
1481 this.fileWatcher_.addEventListener(
1482 'watcher-metadata-changed',
1483 this.onWatcherMetadataChanged_.bind(this));
1485 this.directoryModel_ = new DirectoryModel(
1489 this.metadataCache_,
1490 this.volumeManager_);
1492 this.folderShortcutsModel_ = new FolderShortcutsDataModel(
1493 this.volumeManager_);
1495 this.selectionHandler_ = new FileSelectionHandler(this);
1497 var dataModel = this.directoryModel_.getFileList();
1498 dataModel.addEventListener('permuted',
1499 this.updateStartupPrefs_.bind(this));
1501 this.directoryModel_.getFileListSelection().addEventListener('change',
1502 this.selectionHandler_.onFileSelectionChanged.bind(
1503 this.selectionHandler_));
1505 this.initList_(this.grid_);
1506 this.initList_(this.table_.list);
1508 var fileListFocusBound = this.onFileListFocus_.bind(this);
1509 this.table_.list.addEventListener('focus', fileListFocusBound);
1510 this.grid_.addEventListener('focus', fileListFocusBound);
1512 var draggingBound = this.onDragging_.bind(this);
1513 var dragEndBound = this.onDragEnd_.bind(this);
1515 // Listen to drag events to hide preview panel while user is dragging files.
1516 // Files.app prevents default actions in 'dragstart' in some situations,
1517 // so we listen to 'drag' to know the list is actually being dragged.
1518 this.table_.list.addEventListener('drag', draggingBound);
1519 this.grid_.addEventListener('drag', draggingBound);
1520 this.table_.list.addEventListener('dragend', dragEndBound);
1521 this.grid_.addEventListener('dragend', dragEndBound);
1523 // Listen to dragselection events to hide preview panel while the user is
1524 // selecting files by drag operation.
1525 this.table_.list.addEventListener('dragselectionstart', draggingBound);
1526 this.grid_.addEventListener('dragselectionstart', draggingBound);
1527 this.table_.list.addEventListener('dragselectionend', dragEndBound);
1528 this.grid_.addEventListener('dragselectionend', dragEndBound);
1530 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
1531 // attach the directory model.
1532 this.initDirectoryTree_();
1534 this.table_.addEventListener('column-resize-end',
1535 this.updateStartupPrefs_.bind(this));
1537 // Restore preferences.
1538 this.directoryModel_.getFileList().sort(
1539 this.viewOptions_.sortField || 'modificationTime',
1540 this.viewOptions_.sortDirection || 'desc');
1541 if (this.viewOptions_.columns) {
1542 var cm = this.table_.columnModel;
1543 for (var i = 0; i < cm.totalSize; i++) {
1544 if (this.viewOptions_.columns[i] > 0)
1545 cm.setWidth(i, this.viewOptions_.columns[i]);
1548 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
1550 this.closeOnUnmount_ = (this.params_.action == 'auto-open');
1552 if (this.closeOnUnmount_) {
1553 this.volumeManager_.addEventListener('externally-unmounted',
1554 this.onExternallyUnmounted_.bind(this));
1557 // Update metadata to change 'Today' and 'Yesterday' dates.
1558 var today = new Date();
1560 today.setMinutes(0);
1561 today.setSeconds(0);
1562 today.setMilliseconds(0);
1563 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1564 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
1570 FileManager.prototype.initDirectoryTree_ = function() {
1571 var fakeEntriesVisible =
1572 this.dialogType !== DialogType.SELECT_SAVEAS_FILE;
1573 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
1574 DirectoryTree.decorate(this.directoryTree_,
1575 this.directoryModel_,
1576 this.volumeManager_,
1577 this.metadataCache_,
1578 fakeEntriesVisible);
1579 this.directoryTree_.dataModel = new NavigationListModel(
1580 this.volumeManager_, this.folderShortcutsModel_);
1582 // Visible height of the directory tree depends on the size of progress
1583 // center panel. When the size of progress center panel changes, directory
1584 // tree has to be notified to adjust its components (e.g. progress bar).
1585 var observer = new MutationObserver(
1586 this.directoryTree_.relayout.bind(this.directoryTree_));
1587 observer.observe(this.progressCenterPanel_.element,
1588 {subtree: true, attributes: true, childList: true});
1594 FileManager.prototype.updateStartupPrefs_ = function() {
1595 var sortStatus = this.directoryModel_.getFileList().sortStatus;
1597 sortField: sortStatus.field,
1598 sortDirection: sortStatus.direction,
1600 listType: this.listType_
1602 var cm = this.table_.columnModel;
1603 for (var i = 0; i < cm.totalSize; i++) {
1604 prefs.columns.push(cm.getWidth(i));
1606 // Save the global default.
1608 items[this.startupPrefName_] = JSON.stringify(prefs);
1609 chrome.storage.local.set(items);
1611 // Save the window-specific preference.
1612 if (window.appState) {
1613 window.appState.viewOptions = prefs;
1614 util.saveAppState();
1618 FileManager.prototype.refocus = function() {
1620 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
1621 targetElement = this.filenameInput_;
1623 targetElement = this.currentList_;
1625 // Hack: if the tabIndex is disabled, we can assume a modal dialog is
1626 // shown. Focus to a button on the dialog instead.
1627 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1)
1628 targetElement = document.querySelector('button:not([tabIndex="-1"])');
1631 targetElement.focus();
1635 * File list focus handler. Used to select the top most element on the list
1636 * if nothing was selected.
1640 FileManager.prototype.onFileListFocus_ = function() {
1641 // If the file list is focused by <Tab>, select the first item if no item
1643 if (this.pressingTab_) {
1644 if (this.getSelection() && this.getSelection().totalCount == 0)
1645 this.directoryModel_.selectIndex(0);
1650 * Index of selected item in the typeList of the dialog params.
1652 * @return {number} 1-based index of selected type or 0 if no type selected.
1655 FileManager.prototype.getSelectedFilterIndex_ = function() {
1656 var index = Number(this.fileTypeSelector_.selectedIndex);
1657 if (index < 0) // Nothing selected.
1659 if (this.params_.includeAllFiles) // Already 1-based.
1661 return index + 1; // Convert to 1-based;
1664 FileManager.prototype.setListType = function(type) {
1665 if (type && type == this.listType_)
1668 this.table_.list.startBatchUpdates();
1669 this.grid_.startBatchUpdates();
1671 // TODO(dzvorygin): style.display and dataModel setting order shouldn't
1672 // cause any UI bugs. Currently, the only right way is first to set display
1673 // style and only then set dataModel.
1675 if (type == FileManager.ListType.DETAIL) {
1676 this.table_.dataModel = this.directoryModel_.getFileList();
1677 this.table_.selectionModel = this.directoryModel_.getFileListSelection();
1678 this.table_.hidden = false;
1679 this.grid_.hidden = true;
1680 this.grid_.selectionModel = this.emptySelectionModel_;
1681 this.grid_.dataModel = this.emptyDataModel_;
1682 this.table_.hidden = false;
1683 /** @type {cr.ui.List} */
1684 this.currentList_ = this.table_.list;
1685 this.ui_.toggleViewButton.classList.remove('table');
1686 this.ui_.toggleViewButton.classList.add('grid');
1687 } else if (type == FileManager.ListType.THUMBNAIL) {
1688 this.grid_.dataModel = this.directoryModel_.getFileList();
1689 this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
1690 this.grid_.hidden = false;
1691 this.table_.hidden = true;
1692 this.table_.selectionModel = this.emptySelectionModel_;
1693 this.table_.dataModel = this.emptyDataModel_;
1694 this.grid_.hidden = false;
1695 /** @type {cr.ui.List} */
1696 this.currentList_ = this.grid_;
1697 this.ui_.toggleViewButton.classList.remove('grid');
1698 this.ui_.toggleViewButton.classList.add('table');
1700 throw new Error('Unknown list type: ' + type);
1703 this.listType_ = type;
1704 this.updateStartupPrefs_();
1707 this.table_.list.endBatchUpdates();
1708 this.grid_.endBatchUpdates();
1712 * Initialize the file list table or grid.
1714 * @param {cr.ui.List} list The list.
1717 FileManager.prototype.initList_ = function(list) {
1718 // Overriding the default role 'list' to 'listbox' for better accessibility
1720 list.setAttribute('role', 'listbox');
1721 list.addEventListener('click', this.onDetailClick_.bind(this));
1722 list.id = 'file-list';
1728 FileManager.prototype.onCopyProgress_ = function(event) {
1729 if (event.reason == 'ERROR' &&
1730 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
1731 event.error.data.toDrive &&
1732 event.error.data.name == util.FileError.QUOTA_EXCEEDED_ERR) {
1733 this.alert.showHtml(
1734 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
1735 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
1737 event.error.data.sourceFileUrl.split('/').pop()),
1738 str('GOOGLE_DRIVE_BUY_STORAGE_URL')));
1743 * Handler of file manager operations. Called when an entry has been
1745 * This updates directory model to reflect operation result immediately (not
1746 * waiting for directory update event). Also, preloads thumbnails for the
1747 * images of new entries.
1748 * See also FileOperationManager.EventRouter.
1750 * @param {Event} event An event for the entry change.
1753 FileManager.prototype.onEntriesChanged_ = function(event) {
1754 var kind = event.kind;
1755 var entries = event.entries;
1756 this.directoryModel_.onEntriesChanged(kind, entries);
1757 this.selectionHandler_.onFileSelectionChanged();
1759 if (kind !== util.EntryChangedKind.CREATED)
1762 var preloadThumbnail = function(entry) {
1763 var locationInfo = this.volumeManager_.getLocationInfo(entry);
1766 this.metadataCache_.getOne(entry, 'thumbnail|external',
1767 function(metadata) {
1768 var thumbnailLoader_ = new ThumbnailLoader(
1770 ThumbnailLoader.LoaderType.CANVAS,
1772 undefined, // Media type.
1773 locationInfo.isDriveBased ?
1774 ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1775 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1776 10); // Very low priority.
1777 thumbnailLoader_.loadDetachedImage(function(success) {});
1781 for (var i = 0; i < entries.length; i++) {
1782 // Preload a thumbnail if the new copied entry an image.
1783 if (FileType.isImage(entries[i]))
1784 preloadThumbnail(entries[i]);
1789 * Fills the file type list or hides it.
1792 FileManager.prototype.initFileTypeFilter_ = function() {
1793 if (this.params_.includeAllFiles) {
1794 var option = this.document_.createElement('option');
1795 option.innerText = str('ALL_FILES_FILTER');
1796 this.fileTypeSelector_.appendChild(option);
1800 for (var i = 0; i !== this.fileTypes_.length; i++) {
1801 var fileType = this.fileTypes_[i];
1802 var option = this.document_.createElement('option');
1803 var description = fileType.description;
1805 // See if all the extensions in the group have the same description.
1806 for (var j = 0; j !== fileType.extensions.length; j++) {
1807 var currentDescription = FileType.typeToString(
1808 FileType.getTypeForName('.' + fileType.extensions[j]));
1809 if (!description) // Set the first time.
1810 description = currentDescription;
1811 else if (description != currentDescription) {
1812 // No single description, fall through to the extension list.
1819 // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1820 description = fileType.extensions.map(function(s) {
1824 option.innerText = description;
1826 option.value = i + 1;
1828 if (fileType.selected)
1829 option.selected = true;
1831 this.fileTypeSelector_.appendChild(option);
1834 var options = this.fileTypeSelector_.querySelectorAll('option');
1835 if (options.length >= 2) {
1836 // There is in fact no choice, show the selector.
1837 this.fileTypeSelector_.hidden = false;
1839 this.fileTypeSelector_.addEventListener('change',
1840 this.updateFileTypeFilter_.bind(this));
1845 * Filters file according to the selected file type.
1848 FileManager.prototype.updateFileTypeFilter_ = function() {
1849 this.fileFilter_.removeFilter('fileType');
1850 var selectedIndex = this.getSelectedFilterIndex_();
1851 if (selectedIndex > 0) { // Specific filter selected.
1852 var regexp = new RegExp('\\.(' +
1853 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
1854 var filter = function(entry) {
1855 return entry.isDirectory || regexp.test(entry.name);
1857 this.fileFilter_.addFilter('fileType', filter);
1859 // In save dialog, update the destination name extension.
1860 if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
1861 var current = this.filenameInput_.value;
1862 var newExt = this.fileTypes_[selectedIndex - 1].extensions[0];
1863 if (newExt && !regexp.test(current)) {
1864 var i = current.lastIndexOf('.');
1866 this.filenameInput_.value = current.substr(0, i) + '.' + newExt;
1867 this.selectTargetNameInFilenameInput_();
1875 * Resize details and thumb views to fit the new window size.
1878 FileManager.prototype.onResize_ = function() {
1879 if (this.listType_ == FileManager.ListType.THUMBNAIL)
1880 this.grid_.relayout();
1882 this.table_.relayout();
1884 // May not be available during initialization.
1885 if (this.directoryTree_)
1886 this.directoryTree_.relayout();
1888 this.ui_.locationBreadcrumbs.truncate();
1892 * Handles local metadata changes in the currect directory.
1893 * @param {Event} event Change event.
1896 FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
1897 this.updateMetadataInUI_(
1898 event.metadataType, event.entries, event.properties);
1902 * Resize details and thumb views to fit the new window size.
1905 FileManager.prototype.onPreviewPanelVisibilityChange_ = function() {
1906 // This method may be called on initialization. Some object may not be
1909 var panelHeight = this.previewPanel_.visible ?
1910 this.previewPanel_.height : 0;
1912 this.grid_.setBottomMarginForPanel(panelHeight);
1914 this.table_.setBottomMarginForPanel(panelHeight);
1918 * Invoked while the drag is being performed on the list or the grid.
1919 * Note: this method may be called multiple times before onDragEnd_().
1922 FileManager.prototype.onDragging_ = function() {
1923 // On open file dialog, the preview panel is always shown.
1924 if (DialogType.isOpenDialog(this.dialogType))
1926 this.previewPanel_.visibilityType =
1927 PreviewPanel.VisibilityType.ALWAYS_HIDDEN;
1931 * Invoked when the drag is ended on the list or the grid.
1934 FileManager.prototype.onDragEnd_ = function() {
1935 // On open file dialog, the preview panel is always shown.
1936 if (DialogType.isOpenDialog(this.dialogType))
1938 this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO;
1942 * Sets up the current directory during initialization.
1945 FileManager.prototype.setupCurrentDirectory_ = function() {
1946 var tracker = this.directoryModel_.createDirectoryChangeTracker();
1947 var queue = new AsyncUtil.Queue();
1949 // Wait until the volume manager is initialized.
1950 queue.run(function(callback) {
1952 this.volumeManager_.ensureInitialized(callback);
1955 var nextCurrentDirEntry;
1958 // Resolve the selectionURL to selectionEntry or to currentDirectoryEntry
1959 // in case of being a display root or a default directory to open files.
1960 queue.run(function(callback) {
1961 if (!this.initSelectionURL_) {
1965 webkitResolveLocalFileSystemURL(
1966 this.initSelectionURL_,
1968 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1969 // If location information is not available, then the volume is
1970 // no longer (or never) available.
1971 if (!locationInfo) {
1975 // If the selection is root, then use it as a current directory
1976 // instead. This is because, selecting a root entry is done as
1978 if (locationInfo.isRootEntry)
1979 nextCurrentDirEntry = inEntry;
1981 // If this dialog attempts to open file(s) and the selection is a
1982 // directory, the selection should be the current directory.
1983 if (DialogType.isOpenFileDialog(this.dialogType) &&
1984 inEntry.isDirectory) {
1985 nextCurrentDirEntry = inEntry;
1988 // By default, the selection should be selected entry and the
1989 // parent directory of it should be the current directory.
1990 if (!nextCurrentDirEntry)
1991 selectionEntry = inEntry;
1994 }.bind(this), callback);
1996 // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done
1997 // by the previous step).
1998 queue.run(function(callback) {
1999 if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) {
2003 webkitResolveLocalFileSystemURL(
2004 this.initCurrentDirectoryURL_,
2006 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
2007 if (!locationInfo) {
2011 nextCurrentDirEntry = inEntry;
2013 }.bind(this), callback);
2014 // TODO(mtomasz): Implement reopening on special search, when fake
2015 // entries are converted to directory providers.
2018 // If the directory to be changed to is not available, then first fallback
2019 // to the parent of the selection entry.
2020 queue.run(function(callback) {
2021 if (nextCurrentDirEntry || !selectionEntry) {
2025 selectionEntry.getParent(function(inEntry) {
2026 nextCurrentDirEntry = inEntry;
2031 // Check if the next current directory is not a virtual directory which is
2032 // not available in UI. This may happen to shared on Drive.
2033 queue.run(function(callback) {
2034 if (!nextCurrentDirEntry) {
2038 var locationInfo = this.volumeManager_.getLocationInfo(
2039 nextCurrentDirEntry);
2040 // If we can't check, assume that the directory is illegal.
2041 if (!locationInfo) {
2042 nextCurrentDirEntry = null;
2046 // Having root directory of DRIVE_OTHER here should be only for shared
2047 // with me files. Fallback to Drive root in such case.
2048 if (locationInfo.isRootEntry && locationInfo.rootType ===
2049 VolumeManagerCommon.RootType.DRIVE_OTHER) {
2050 var volumeInfo = this.volumeManager_.getVolumeInfo(nextCurrentDirEntry);
2052 nextCurrentDirEntry = null;
2056 volumeInfo.resolveDisplayRoot().then(
2058 nextCurrentDirEntry = entry;
2060 }).catch(function(error) {
2061 console.error(error.stack || error);
2062 nextCurrentDirEntry = null;
2070 // If the directory to be changed to is still not resolved, then fallback
2071 // to the default display root.
2072 queue.run(function(callback) {
2073 if (nextCurrentDirEntry) {
2077 this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) {
2078 nextCurrentDirEntry = displayRoot;
2083 // If selection failed to be resolved (eg. didn't exist, in case of saving
2084 // a file, or in case of a fallback of the current directory, then try to
2085 // resolve again using the target name.
2086 queue.run(function(callback) {
2087 if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) {
2091 // Try to resolve as a file first. If it fails, then as a directory.
2092 nextCurrentDirEntry.getFile(
2093 this.initTargetName_,
2095 function(targetEntry) {
2096 selectionEntry = targetEntry;
2099 // Failed to resolve as a file
2100 nextCurrentDirEntry.getDirectory(
2101 this.initTargetName_,
2103 function(targetEntry) {
2104 selectionEntry = targetEntry;
2107 // Failed to resolve as either file or directory.
2114 queue.run(function(callback) {
2115 // Check directory change.
2117 if (tracker.hasChanged) {
2121 // Finish setup current directory.
2122 this.finishSetupCurrentDirectory_(
2123 nextCurrentDirEntry,
2125 this.initTargetName_);
2131 * @param {DirectoryEntry} directoryEntry Directory to be opened.
2132 * @param {Entry=} opt_selectionEntry Entry to be selected.
2133 * @param {string=} opt_suggestedName Suggested name for a non-existing\
2137 FileManager.prototype.finishSetupCurrentDirectory_ = function(
2138 directoryEntry, opt_selectionEntry, opt_suggestedName) {
2139 // Open the directory, and select the selection (if passed).
2140 if (util.isFakeEntry(directoryEntry)) {
2141 this.directoryModel_.specialSearch(directoryEntry, '');
2143 this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
2144 if (opt_selectionEntry)
2145 this.directoryModel_.selectEntry(opt_selectionEntry);
2149 if (this.dialogType === DialogType.FULL_PAGE) {
2150 // In the FULL_PAGE mode if the restored URL points to a file we might
2151 // have to invoke a task after selecting it.
2152 if (this.params_.action === 'select')
2157 // TODO(mtomasz): Implement remounting archives after crash.
2158 // See: crbug.com/333139
2160 // If there is a task to be run, run it after the scan is completed.
2162 var listener = function() {
2163 if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(),
2165 // Opened on a different URL. Probably fallbacked. Therefore,
2166 // do not invoke a task.
2169 this.directoryModel_.removeEventListener(
2170 'scan-completed', listener);
2173 this.directoryModel_.addEventListener('scan-completed', listener);
2175 } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
2176 this.filenameInput_.value = opt_suggestedName || '';
2177 this.selectTargetNameInFilenameInput_();
2184 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
2185 var entries = this.directoryModel_.getFileList().slice();
2186 var directoryEntry = this.directoryModel_.getCurrentDirEntry();
2187 if (!directoryEntry)
2189 // We don't pass callback here. When new metadata arrives, we have an
2190 // observer registered to update the UI.
2192 // TODO(dgozman): refresh content metadata only when modificationTime
2194 var isFakeEntry = util.isFakeEntry(directoryEntry);
2195 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
2197 this.metadataCache_.clearRecursively(directoryEntry, '*');
2198 this.metadataCache_.get(getEntries, 'filesystem|external', null);
2200 var visibleItems = this.currentList_.items;
2201 var visibleEntries = [];
2202 for (var i = 0; i < visibleItems.length; i++) {
2203 var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
2204 var entry = this.directoryModel_.getFileList().item(index);
2205 // The following check is a workaround for the bug in list: sometimes item
2206 // does not have listIndex, and therefore is not found in the list.
2207 if (entry) visibleEntries.push(entry);
2209 // Refreshes the metadata.
2210 this.metadataCache_.getLatest(visibleEntries, 'thumbnail', null);
2216 FileManager.prototype.dailyUpdateModificationTime_ = function() {
2217 var entries = this.directoryModel_.getFileList().slice();
2218 this.metadataCache_.get(
2222 this.updateMetadataInUI_('filesystem', entries);
2225 setTimeout(this.dailyUpdateModificationTime_.bind(this),
2226 MILLISECONDS_IN_DAY);
2230 * @param {string} type Type of metadata changed.
2231 * @param {Array.<Entry>} entries Array of entries.
2234 FileManager.prototype.updateMetadataInUI_ = function(type, entries) {
2235 if (this.listType_ == FileManager.ListType.DETAIL)
2236 this.table_.updateListItemsMetadata(type, entries);
2238 this.grid_.updateListItemsMetadata(type, entries);
2239 // TODO: update bottom panel thumbnails.
2243 * Restore the item which is being renamed while refreshing the file list. Do
2244 * nothing if no item is being renamed or such an item disappeared.
2246 * While refreshing file list it gets repopulated with new file entries.
2247 * There is not a big difference whether DOM items stay the same or not.
2248 * Except for the item that the user is renaming.
2252 FileManager.prototype.restoreItemBeingRenamed_ = function() {
2253 if (!this.isRenamingInProgress())
2256 var dm = this.directoryModel_;
2257 var leadIndex = dm.getFileListSelection().leadIndex;
2261 var leadEntry = dm.getFileList().item(leadIndex);
2262 if (!util.isSameEntry(this.renameInput_.currentEntry, leadEntry))
2265 var leadListItem = this.findListItemForNode_(this.renameInput_);
2266 if (this.currentList_ == this.table_.list) {
2267 this.table_.updateFileMetadata(leadListItem, leadEntry);
2269 this.currentList_.restoreLeadItem(leadListItem);
2273 * TODO(mtomasz): Move this to a utility function working on the root type.
2274 * @return {boolean} True if the current directory content is from Google
2277 FileManager.prototype.isOnDrive = function() {
2278 var rootType = this.directoryModel_.getCurrentRootType();
2279 return rootType === VolumeManagerCommon.RootType.DRIVE ||
2280 rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME ||
2281 rootType === VolumeManagerCommon.RootType.DRIVE_RECENT ||
2282 rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE;
2286 * Check if the drive-related setting items should be shown on currently
2287 * displayed gear menu.
2288 * @return {boolean} True if those setting items should be shown.
2290 FileManager.prototype.shouldShowDriveSettings = function() {
2291 return this.isOnDrive() && this.isSecretGearMenuShown_;
2295 * Overrides default handling for clicks on hyperlinks.
2296 * In a packaged apps links with targer='_blank' open in a new tab by
2297 * default, other links do not open at all.
2299 * @param {Event} event Click event.
2302 FileManager.prototype.onExternalLinkClick_ = function(event) {
2303 if (event.target.tagName != 'A' || !event.target.href)
2306 if (this.dialogType != DialogType.FULL_PAGE)
2311 * Task combobox handler.
2313 * @param {Object} event Event containing task which was clicked.
2316 FileManager.prototype.onTaskItemClicked_ = function(event) {
2317 var selection = this.getSelection();
2318 if (!selection.tasks) return;
2320 if (event.item.task) {
2321 // Task field doesn't exist on change-default dropdown item.
2322 selection.tasks.execute(event.item.task.taskId);
2324 var extensions = [];
2326 for (var i = 0; i < selection.entries.length; i++) {
2327 var match = /\.(\w+)$/g.exec(selection.entries[i].toURL());
2329 var ext = match[1].toUpperCase();
2330 if (extensions.indexOf(ext) == -1) {
2331 extensions.push(ext);
2338 if (extensions.length == 1) {
2339 format = extensions[0];
2342 // Change default was clicked. We should open "change default" dialog.
2343 selection.tasks.showTaskPicker(this.defaultTaskPicker,
2344 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
2345 strf('CHANGE_DEFAULT_CAPTION', format),
2346 this.onDefaultTaskDone_.bind(this));
2351 * Sets the given task as default, when this task is applicable.
2353 * @param {Object} task Task to set as default.
2356 FileManager.prototype.onDefaultTaskDone_ = function(task) {
2357 // TODO(dgozman): move this method closer to tasks.
2358 var selection = this.getSelection();
2359 // TODO(mtomasz): Move conversion from entry to url to custom bindings.
2360 // crbug.com/345527.
2361 chrome.fileManagerPrivate.setDefaultTask(
2363 util.entriesToURLs(selection.entries),
2364 selection.mimeTypes);
2365 selection.tasks = new FileTasks(this);
2366 selection.tasks.init(selection.entries, selection.mimeTypes);
2367 selection.tasks.display(this.taskItems_);
2368 this.refreshCurrentDirectoryMetadata_();
2369 this.selectionHandler_.onFileSelectionChanged();
2375 FileManager.prototype.onPreferencesChanged_ = function() {
2377 this.getPreferences_(function(prefs) {
2378 self.initDateTimeFormatters_();
2379 self.refreshCurrentDirectoryMetadata_();
2381 if (prefs.cellularDisabled)
2382 self.syncButton.setAttribute('checked', '');
2384 self.syncButton.removeAttribute('checked');
2386 if (self.hostedButton.hasAttribute('checked') ===
2387 prefs.hostedFilesDisabled && self.isOnDrive()) {
2388 self.directoryModel_.rescan(false);
2391 if (!prefs.hostedFilesDisabled)
2392 self.hostedButton.setAttribute('checked', '');
2394 self.hostedButton.removeAttribute('checked');
2396 true /* refresh */);
2399 FileManager.prototype.onDriveConnectionChanged_ = function() {
2400 var connection = this.volumeManager_.getDriveConnectionState();
2401 if (this.commandHandler)
2402 this.commandHandler.updateAvailability();
2403 if (this.dialogContainer_)
2404 this.dialogContainer_.setAttribute('connection', connection.type);
2405 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
2406 this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
2410 * Tells whether the current directory is read only.
2411 * TODO(mtomasz): Remove and use EntryLocation directly.
2412 * @return {boolean} True if read only, false otherwise.
2414 FileManager.prototype.isOnReadonlyDirectory = function() {
2415 return this.directoryModel_.isReadOnly();
2419 * @param {Event} event Unmount event.
2422 FileManager.prototype.onExternallyUnmounted_ = function(event) {
2423 if (event.volumeInfo === this.currentVolumeInfo_) {
2424 if (this.closeOnUnmount_) {
2425 // If the file manager opened automatically when a usb drive inserted,
2426 // user have never changed current volume (that implies the current
2427 // directory is still on the device) then close this window.
2434 * @return {Array.<Entry>} List of all entries in the current directory.
2436 FileManager.prototype.getAllEntriesInCurrentDirectory = function() {
2437 return this.directoryModel_.getFileList().slice();
2440 FileManager.prototype.isRenamingInProgress = function() {
2441 return !!this.renameInput_.currentEntry;
2447 FileManager.prototype.focusCurrentList_ = function() {
2448 if (this.listType_ == FileManager.ListType.DETAIL)
2449 this.table_.focus();
2450 else // this.listType_ == FileManager.ListType.THUMBNAIL)
2455 * Return DirectoryEntry of the current directory or null.
2456 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
2457 * null if the directory model is not ready or the current directory is
2460 FileManager.prototype.getCurrentDirectoryEntry = function() {
2461 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
2465 * Shows the share dialog for the selected file or directory.
2467 FileManager.prototype.shareSelection = function() {
2468 var entries = this.getSelection().entries;
2469 if (entries.length != 1) {
2470 console.warn('Unable to share multiple items at once.');
2473 // Add the overlapped class to prevent the applicaiton window from
2474 // captureing mouse events.
2475 this.shareDialog_.show(entries[0], function(result) {
2476 if (result == ShareDialog.Result.NETWORK_ERROR)
2477 this.error.show(str('SHARE_ERROR'));
2482 * Creates a folder shortcut.
2483 * @param {Entry} entry A shortcut which refers to |entry| to be created.
2485 FileManager.prototype.createFolderShortcut = function(entry) {
2487 if (this.folderShortcutExists(entry))
2490 this.folderShortcutsModel_.add(entry);
2494 * Checkes if the shortcut which refers to the given folder exists or not.
2495 * @param {Entry} entry Entry of the folder to be checked.
2497 FileManager.prototype.folderShortcutExists = function(entry) {
2498 return this.folderShortcutsModel_.exists(entry);
2502 * Removes the folder shortcut.
2503 * @param {Entry} entry The shortcut which refers to |entry| is to be removed.
2505 FileManager.prototype.removeFolderShortcut = function(entry) {
2506 this.folderShortcutsModel_.remove(entry);
2510 * Blinks the selection. Used to give feedback when copying or cutting the
2513 FileManager.prototype.blinkSelection = function() {
2514 var selection = this.getSelection();
2515 if (!selection || selection.totalCount == 0)
2518 for (var i = 0; i < selection.entries.length; i++) {
2519 var selectedIndex = selection.indexes[i];
2520 var listItem = this.currentList_.getListItemByIndex(selectedIndex);
2522 this.blinkListItem_(listItem);
2527 * @param {Element} listItem List item element.
2530 FileManager.prototype.blinkListItem_ = function(listItem) {
2531 listItem.classList.add('blink');
2532 setTimeout(function() {
2533 listItem.classList.remove('blink');
2540 FileManager.prototype.selectTargetNameInFilenameInput_ = function() {
2541 var input = this.filenameInput_;
2543 var selectionEnd = input.value.lastIndexOf('.');
2544 if (selectionEnd == -1) {
2547 input.selectionStart = 0;
2548 input.selectionEnd = selectionEnd;
2553 * Handles mouse click or tap.
2555 * @param {Event} event The click event.
2558 FileManager.prototype.onDetailClick_ = function(event) {
2559 if (this.isRenamingInProgress()) {
2560 // Don't pay attention to clicks during a rename.
2564 var listItem = this.findListItemForEvent_(event);
2565 var selection = this.getSelection();
2566 if (!listItem || !listItem.selected || selection.totalCount != 1) {
2570 // React on double click, but only if both clicks hit the same item.
2571 // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2572 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
2573 this.lastClickedItem_ = listItem;
2575 if (event.detail != clickNumber)
2578 var entry = selection.entries[0];
2579 if (entry.isDirectory) {
2580 this.onDirectoryAction_(entry);
2582 this.dispatchSelectionAction_();
2589 FileManager.prototype.dispatchSelectionAction_ = function() {
2590 if (this.dialogType == DialogType.FULL_PAGE) {
2591 var selection = this.getSelection();
2592 var tasks = selection.tasks;
2593 var urls = selection.urls;
2594 var mimeTypes = selection.mimeTypes;
2596 tasks.executeDefault();
2599 if (!this.okButton_.disabled) {
2607 * Opens the suggest file dialog.
2609 * @param {Entry} entry Entry of the file.
2610 * @param {function()} onSuccess Success callback.
2611 * @param {function()} onCancelled User-cancelled callback.
2612 * @param {function()} onFailure Failure callback.
2615 FileManager.prototype.openSuggestAppsDialog =
2616 function(entry, onSuccess, onCancelled, onFailure) {
2622 this.metadataCache_.getOne(entry, 'external', function(prop) {
2623 if (!prop || !prop.contentMimeType) {
2628 var basename = entry.name;
2629 var splitted = util.splitExtension(basename);
2630 var filename = splitted[0];
2631 var extension = splitted[1];
2632 var mime = prop.contentMimeType;
2634 // Returns with failure if the file has neither extension nor mime.
2635 if (!extension || !mime) {
2640 var onDialogClosed = function(result) {
2642 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
2645 case SuggestAppsDialog.Result.FAILED:
2653 if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
2654 this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
2656 this.suggestAppsDialog.showByExtensionAndMime(
2657 extension, mime, onDialogClosed);
2663 * Called when a dialog is shown or hidden.
2664 * @param {boolean} show True if a dialog is shown, false if hidden.
2666 FileManager.prototype.onDialogShownOrHidden = function(show) {
2668 // If a dialog is shown, activate the window.
2669 var appWindow = chrome.app.window.current();
2674 // Set/unset a flag to disable dragging on the title area.
2675 this.dialogContainer_.classList.toggle('disable-header-drag', show);
2679 * Executes directory action (i.e. changes directory).
2681 * @param {DirectoryEntry} entry Directory entry to which directory should be
2685 FileManager.prototype.onDirectoryAction_ = function(entry) {
2686 return this.directoryModel_.changeDirectoryEntry(entry);
2690 * Update the window title.
2693 FileManager.prototype.updateTitle_ = function() {
2694 if (this.dialogType != DialogType.FULL_PAGE)
2697 if (!this.currentVolumeInfo_)
2700 this.document_.title = this.currentVolumeInfo_.label;
2704 * Updates the location information displayed on the toolbar.
2705 * @param {DirectoryEntry=} opt_entry Directory entry to be displayed as
2706 * current location. Default entry is the current directory.
2709 FileManager.prototype.updateLocationLine_ = function(opt_entry) {
2710 var entry = opt_entry || this.getCurrentDirectoryEntry();
2711 // Updates volume icon.
2712 var location = this.volumeManager_.getLocationInfo(entry);
2713 if (location && location.rootType && location.isRootEntry) {
2714 this.ui_.locationVolumeIcon.setAttribute(
2715 'volume-type-icon', location.rootType);
2716 this.ui_.locationVolumeIcon.removeAttribute('volume-subtype');
2718 this.ui_.locationVolumeIcon.setAttribute(
2719 'volume-type-icon', location.volumeInfo.volumeType);
2720 this.ui_.locationVolumeIcon.setAttribute(
2721 'volume-subtype', location.volumeInfo.deviceType);
2723 // Updates breadcrumbs.
2724 this.ui_.locationBreadcrumbs.show(entry);
2728 * Update the gear menu.
2731 FileManager.prototype.updateGearMenu_ = function() {
2732 this.refreshRemainingSpace_(true); // Show loading caption.
2736 * Refreshes space info of the current volume.
2737 * @param {boolean} showLoadingCaption Whether show loading caption or not.
2740 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
2741 if (!this.currentVolumeInfo_)
2744 var volumeSpaceInfo =
2745 this.dialogDom_.querySelector('#volume-space-info');
2746 var volumeSpaceInfoSeparator =
2747 this.dialogDom_.querySelector('#volume-space-info-separator');
2748 var volumeSpaceInfoLabel =
2749 this.dialogDom_.querySelector('#volume-space-info-label');
2750 var volumeSpaceInnerBar =
2751 this.dialogDom_.querySelector('#volume-space-info-bar');
2752 var volumeSpaceOuterBar =
2753 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
2755 var currentVolumeInfo = this.currentVolumeInfo_;
2757 // TODO(mtomasz): Add support for remaining space indication for provided
2759 if (currentVolumeInfo.volumeType ==
2760 VolumeManagerCommon.VolumeType.PROVIDED) {
2761 volumeSpaceInfo.hidden = true;
2762 volumeSpaceInfoSeparator.hidden = true;
2766 volumeSpaceInfo.hidden = false;
2767 volumeSpaceInfoSeparator.hidden = false;
2768 volumeSpaceInnerBar.setAttribute('pending', '');
2770 if (showLoadingCaption) {
2771 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2772 volumeSpaceInnerBar.style.width = '100%';
2775 chrome.fileManagerPrivate.getSizeStats(
2776 currentVolumeInfo.volumeId, function(result) {
2777 var volumeInfo = this.volumeManager_.getVolumeInfo(
2778 this.directoryModel_.getCurrentDirEntry());
2779 if (currentVolumeInfo !== this.currentVolumeInfo_)
2781 updateSpaceInfo(result,
2782 volumeSpaceInnerBar,
2783 volumeSpaceInfoLabel,
2784 volumeSpaceOuterBar);
2789 * Update the UI when the current directory changes.
2791 * @param {Event} event The directory-changed event.
2794 FileManager.prototype.onDirectoryChanged_ = function(event) {
2795 var oldCurrentVolumeInfo = this.currentVolumeInfo_;
2797 // Remember the current volume info.
2798 this.currentVolumeInfo_ = this.volumeManager_.getVolumeInfo(
2801 // If volume has changed, then update the gear menu.
2802 if (oldCurrentVolumeInfo !== this.currentVolumeInfo_) {
2803 this.updateGearMenu_();
2804 // If the volume has changed, and it was previously set, then do not
2805 // close on unmount anymore.
2806 if (oldCurrentVolumeInfo)
2807 this.closeOnUnmount_ = false;
2810 this.selectionHandler_.onFileSelectionChanged();
2811 this.ui_.searchBox.clear();
2812 // TODO(mtomasz): Consider remembering the selection.
2813 util.updateAppState(
2814 this.getCurrentDirectoryEntry() ?
2815 this.getCurrentDirectoryEntry().toURL() : '',
2816 '' /* selectionURL */,
2817 '' /* opt_param */);
2819 if (this.commandHandler)
2820 this.commandHandler.updateAvailability();
2822 this.updateUnformattedVolumeStatus_();
2823 this.updateTitle_();
2824 this.updateLocationLine_();
2826 var currentEntry = this.getCurrentDirectoryEntry();
2827 this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ?
2828 null : currentEntry;
2831 FileManager.prototype.updateUnformattedVolumeStatus_ = function() {
2832 var volumeInfo = this.volumeManager_.getVolumeInfo(
2833 this.directoryModel_.getCurrentDirEntry());
2835 if (volumeInfo && volumeInfo.error) {
2836 this.dialogDom_.setAttribute('unformatted', '');
2838 var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2839 if (volumeInfo.error ===
2840 VolumeManagerCommon.VolumeError.UNSUPPORTED_FILESYSTEM) {
2841 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2843 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2846 // Update 'canExecute' for format command so the format button's disabled
2847 // property is properly set.
2848 if (this.commandHandler)
2849 this.commandHandler.updateAvailability();
2851 this.dialogDom_.removeAttribute('unformatted');
2855 FileManager.prototype.findListItemForEvent_ = function(event) {
2856 return this.findListItemForNode_(event.touchedElement || event.srcElement);
2859 FileManager.prototype.findListItemForNode_ = function(node) {
2860 var item = this.currentList_.getListItemAncestor(node);
2861 // TODO(serya): list should check that.
2862 return item && this.currentList_.isItem(item) ? item : null;
2866 * Unload handler for the page.
2869 FileManager.prototype.onUnload_ = function() {
2870 if (this.directoryModel_)
2871 this.directoryModel_.dispose();
2872 if (this.volumeManager_)
2873 this.volumeManager_.dispose();
2875 i < this.fileTransferController_.pendingTaskIds.length;
2877 var taskId = this.fileTransferController_.pendingTaskIds[i];
2879 this.backgroundPage_.background.progressCenter.getItemById(taskId);
2881 item.state = ProgressItemState.CANCELED;
2882 this.backgroundPage_.background.progressCenter.updateItem(item);
2884 if (this.progressCenterPanel_) {
2885 this.backgroundPage_.background.progressCenter.removePanel(
2886 this.progressCenterPanel_);
2888 if (this.fileOperationManager_) {
2889 if (this.onCopyProgressBound_) {
2890 this.fileOperationManager_.removeEventListener(
2891 'copy-progress', this.onCopyProgressBound_);
2893 if (this.onEntriesChangedBound_) {
2894 this.fileOperationManager_.removeEventListener(
2895 'entries-changed', this.onEntriesChangedBound_);
2898 window.closing = true;
2899 if (this.backgroundPage_)
2900 this.backgroundPage_.background.tryClose();
2903 FileManager.prototype.initiateRename = function() {
2904 var item = this.currentList_.ensureLeadItemExists();
2907 var label = item.querySelector('.filename-label');
2908 var input = this.renameInput_;
2909 var currentEntry = this.currentList_.dataModel.item(item.listIndex);
2911 input.value = label.textContent;
2912 item.setAttribute('renaming', '');
2913 label.parentNode.appendChild(input);
2916 var selectionEnd = input.value.lastIndexOf('.');
2917 if (currentEntry.isFile && selectionEnd !== -1) {
2918 input.selectionStart = 0;
2919 input.selectionEnd = selectionEnd;
2924 // This has to be set late in the process so we don't handle spurious
2926 input.currentEntry = currentEntry;
2927 this.table_.startBatchUpdates();
2928 this.grid_.startBatchUpdates();
2932 * @type {Event} Key event.
2935 FileManager.prototype.onRenameInputKeyDown_ = function(event) {
2936 if (!this.isRenamingInProgress())
2939 // Do not move selection or lead item in list during rename.
2940 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
2941 event.stopPropagation();
2944 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
2945 case 'U+001B': // Escape
2946 this.cancelRename_();
2947 event.preventDefault();
2951 this.commitRename_();
2952 event.preventDefault();
2958 * @type {Event} Blur event.
2961 FileManager.prototype.onRenameInputBlur_ = function(event) {
2962 if (this.isRenamingInProgress() && !this.renameInput_.validation_)
2963 this.commitRename_();
2969 FileManager.prototype.commitRename_ = function() {
2970 var input = this.renameInput_;
2971 var entry = input.currentEntry;
2972 var newName = input.value;
2974 if (newName == entry.name) {
2975 this.cancelRename_();
2979 var renamedItemElement = this.findListItemForNode_(this.renameInput_);
2980 var nameNode = renamedItemElement.querySelector('.filename-label');
2982 input.validation_ = true;
2983 var validationDone = function(valid) {
2984 input.validation_ = false;
2987 // Cancel rename if it fails to restore focus from alert dialog.
2988 // Otherwise, just cancel the commitment and continue to rename.
2989 if (this.document_.activeElement != input)
2990 this.cancelRename_();
2994 // Validation succeeded. Do renaming.
2995 this.renameInput_.currentEntry = null;
2996 if (this.renameInput_.parentNode)
2997 this.renameInput_.parentNode.removeChild(this.renameInput_);
2998 renamedItemElement.setAttribute('renaming', 'provisional');
3000 // Optimistically apply new name immediately to avoid flickering in
3002 nameNode.textContent = newName;
3006 function(newEntry) {
3007 this.directoryModel_.onRenameEntry(entry, newEntry);
3008 renamedItemElement.removeAttribute('renaming');
3009 this.table_.endBatchUpdates();
3010 this.grid_.endBatchUpdates();
3011 // Focus may go out of the list. Back it to the list.
3012 this.currentList_.focus();
3015 // Write back to the old name.
3016 nameNode.textContent = entry.name;
3017 renamedItemElement.removeAttribute('renaming');
3018 this.table_.endBatchUpdates();
3019 this.grid_.endBatchUpdates();
3021 // Show error dialog.
3023 if (error.name == util.FileError.PATH_EXISTS_ERR ||
3024 error.name == util.FileError.TYPE_MISMATCH_ERR) {
3025 // Check the existing entry is file or not.
3026 // 1) If the entry is a file:
3027 // a) If we get PATH_EXISTS_ERR, a file exists.
3028 // b) If we get TYPE_MISMATCH_ERR, a directory exists.
3029 // 2) If the entry is a directory:
3030 // a) If we get PATH_EXISTS_ERR, a directory exists.
3031 // b) If we get TYPE_MISMATCH_ERR, a file exists.
3033 (entry.isFile && error.name ==
3034 util.FileError.PATH_EXISTS_ERR) ||
3035 (!entry.isFile && error.name ==
3036 util.FileError.TYPE_MISMATCH_ERR) ?
3037 'FILE_ALREADY_EXISTS' :
3038 'DIRECTORY_ALREADY_EXISTS',
3041 message = strf('ERROR_RENAMING', entry.name,
3042 util.getFileErrorString(error.name));
3045 this.alert.show(message);
3049 // TODO(haruki): this.getCurrentDirectoryEntry() might not return the actual
3050 // parent if the directory content is a search result. Fix it to do proper
3052 this.validateFileName_(this.getCurrentDirectoryEntry(),
3054 validationDone.bind(this));
3060 FileManager.prototype.cancelRename_ = function() {
3061 this.renameInput_.currentEntry = null;
3063 var item = this.findListItemForNode_(this.renameInput_);
3065 item.removeAttribute('renaming');
3067 var parent = this.renameInput_.parentNode;
3069 parent.removeChild(this.renameInput_);
3071 this.table_.endBatchUpdates();
3072 this.grid_.endBatchUpdates();
3074 // Focus may go out of the list. Back it to the list.
3075 this.currentList_.focus();
3081 FileManager.prototype.onFilenameInputInput_ = function() {
3082 this.selectionHandler_.updateOkButton();
3086 * @param {Event} event Key event.
3089 FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
3090 if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */)
3091 this.okButton_.click();
3095 * @param {Event} event Focus event.
3098 FileManager.prototype.onFilenameInputFocus_ = function(event) {
3099 var input = this.filenameInput_;
3101 // On focus we want to select everything but the extension, but
3102 // Chrome will select-all after the focus event completes. We
3103 // schedule a timeout to alter the focus after that happens.
3104 setTimeout(function() {
3105 var selectionEnd = input.value.lastIndexOf('.');
3106 if (selectionEnd == -1) {
3109 input.selectionStart = 0;
3110 input.selectionEnd = selectionEnd;
3118 FileManager.prototype.onScanStarted_ = function() {
3119 if (this.scanInProgress_) {
3120 this.table_.list.endBatchUpdates();
3121 this.grid_.endBatchUpdates();
3124 if (this.commandHandler)
3125 this.commandHandler.updateAvailability();
3126 this.table_.list.startBatchUpdates();
3127 this.grid_.startBatchUpdates();
3128 this.scanInProgress_ = true;
3130 this.scanUpdatedAtLeastOnceOrCompleted_ = false;
3131 if (this.scanCompletedTimer_) {
3132 clearTimeout(this.scanCompletedTimer_);
3133 this.scanCompletedTimer_ = 0;
3136 if (this.scanUpdatedTimer_) {
3137 clearTimeout(this.scanUpdatedTimer_);
3138 this.scanUpdatedTimer_ = 0;
3141 if (this.spinner_.hidden) {
3142 this.cancelSpinnerTimeout_();
3143 this.showSpinnerTimeout_ =
3144 setTimeout(this.showSpinner_.bind(this, true), 500);
3151 FileManager.prototype.onScanCompleted_ = function() {
3152 if (!this.scanInProgress_) {
3153 console.error('Scan-completed event recieved. But scan is not started.');
3157 if (this.commandHandler)
3158 this.commandHandler.updateAvailability();
3159 this.hideSpinnerLater_();
3161 if (this.scanUpdatedTimer_) {
3162 clearTimeout(this.scanUpdatedTimer_);
3163 this.scanUpdatedTimer_ = 0;
3166 // To avoid flickering postpone updating the ui by a small amount of time.
3167 // There is a high chance, that metadata will be received within 50 ms.
3168 this.scanCompletedTimer_ = setTimeout(function() {
3169 // Check if batch updates are already finished by onScanUpdated_().
3170 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
3171 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
3174 this.scanInProgress_ = false;
3175 this.table_.list.endBatchUpdates();
3176 this.grid_.endBatchUpdates();
3177 this.scanCompletedTimer_ = 0;
3184 FileManager.prototype.onScanUpdated_ = function() {
3185 if (!this.scanInProgress_) {
3186 console.error('Scan-updated event recieved. But scan is not started.');
3190 if (this.scanUpdatedTimer_ || this.scanCompletedTimer_)
3193 // Show contents incrementally by finishing batch updated, but only after
3194 // 200ms elapsed, to avoid flickering when it is not necessary.
3195 this.scanUpdatedTimer_ = setTimeout(function() {
3196 // We need to hide the spinner only once.
3197 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
3198 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
3199 this.hideSpinnerLater_();
3203 if (this.scanInProgress_) {
3204 this.table_.list.endBatchUpdates();
3205 this.grid_.endBatchUpdates();
3206 this.table_.list.startBatchUpdates();
3207 this.grid_.startBatchUpdates();
3209 this.scanUpdatedTimer_ = 0;
3216 FileManager.prototype.onScanCancelled_ = function() {
3217 if (!this.scanInProgress_) {
3218 console.error('Scan-cancelled event recieved. But scan is not started.');
3222 if (this.commandHandler)
3223 this.commandHandler.updateAvailability();
3224 this.hideSpinnerLater_();
3225 if (this.scanCompletedTimer_) {
3226 clearTimeout(this.scanCompletedTimer_);
3227 this.scanCompletedTimer_ = 0;
3229 if (this.scanUpdatedTimer_) {
3230 clearTimeout(this.scanUpdatedTimer_);
3231 this.scanUpdatedTimer_ = 0;
3233 // Finish unfinished batch updates.
3234 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
3235 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
3238 this.scanInProgress_ = false;
3239 this.table_.list.endBatchUpdates();
3240 this.grid_.endBatchUpdates();
3244 * Handle the 'rescan-completed' from the DirectoryModel.
3247 FileManager.prototype.onRescanCompleted_ = function() {
3248 this.selectionHandler_.onFileSelectionChanged();
3254 FileManager.prototype.cancelSpinnerTimeout_ = function() {
3255 if (this.showSpinnerTimeout_) {
3256 clearTimeout(this.showSpinnerTimeout_);
3257 this.showSpinnerTimeout_ = 0;
3264 FileManager.prototype.hideSpinnerLater_ = function() {
3265 this.cancelSpinnerTimeout_();
3266 this.showSpinner_(false);
3270 * @param {boolean} on True to show, false to hide.
3273 FileManager.prototype.showSpinner_ = function(on) {
3274 if (on && this.directoryModel_ && this.directoryModel_.isScanning())
3275 this.spinner_.hidden = false;
3277 if (!on && (!this.directoryModel_ ||
3278 !this.directoryModel_.isScanning() ||
3279 this.directoryModel_.getFileList().length != 0)) {
3280 this.spinner_.hidden = true;
3284 FileManager.prototype.createNewFolder = function() {
3285 var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
3287 // Find a name that doesn't exist in the data model.
3288 var files = this.directoryModel_.getFileList();
3290 for (var i = 0; i < files.length; i++) {
3291 var name = files.item(i).name;
3292 // Filtering names prevents from conflicts with prototype's names
3294 if (name.substring(0, defaultName.length) == defaultName)
3298 var baseName = defaultName;
3303 var advance = function() {
3309 var current = function() {
3310 return baseName + separator + index + suffix;
3313 // Accessing hasOwnProperty is safe since hash properties filtered.
3314 while (hash.hasOwnProperty(current())) {
3319 var list = self.currentList_;
3320 var tryCreate = function() {
3323 var onSuccess = function(entry) {
3324 metrics.recordUserAction('CreateNewFolder');
3325 list.selectedItem = entry;
3327 self.table_.list.endBatchUpdates();
3328 self.grid_.endBatchUpdates();
3330 self.initiateRename();
3333 var onError = function(error) {
3334 self.table_.list.endBatchUpdates();
3335 self.grid_.endBatchUpdates();
3337 self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
3338 util.getFileErrorString(error.name)));
3341 var onAbort = function() {
3342 self.table_.list.endBatchUpdates();
3343 self.grid_.endBatchUpdates();
3346 this.table_.list.startBatchUpdates();
3347 this.grid_.startBatchUpdates();
3348 this.directoryModel_.createDirectory(current(),
3355 * Handles click event on the toggle-view button.
3356 * @param {Event} event Click event.
3359 FileManager.prototype.onToggleViewButtonClick_ = function(event) {
3360 if (this.listType_ === FileManager.ListType.DETAIL)
3361 this.setListType(FileManager.ListType.THUMBNAIL);
3363 this.setListType(FileManager.ListType.DETAIL);
3365 event.target.blur();
3369 * KeyDown event handler for the document.
3370 * @param {Event} event Key event.
3373 FileManager.prototype.onKeyDown_ = function(event) {
3374 if (event.keyCode === 9) // Tab
3375 this.pressingTab_ = true;
3376 if (event.keyCode === 17) // Ctrl
3377 this.pressingCtrl_ = true;
3379 if (event.srcElement === this.renameInput_) {
3380 // Ignore keydown handler in the rename input box.
3384 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
3385 case 'Ctrl-U+00BE': // Ctrl-. => Toggle filter files.
3386 this.fileFilter_.setFilterHidden(
3387 !this.fileFilter_.isFilterHiddenOn());
3388 event.preventDefault();
3391 case 'U+001B': // Escape => Cancel dialog.
3392 if (this.dialogType != DialogType.FULL_PAGE) {
3393 // If there is nothing else for ESC to do, then cancel the dialog.
3394 event.preventDefault();
3395 this.cancelButton_.click();
3402 * KeyUp event handler for the document.
3403 * @param {Event} event Key event.
3406 FileManager.prototype.onKeyUp_ = function(event) {
3407 if (event.keyCode === 9) // Tab
3408 this.pressingTab_ = false;
3409 if (event.keyCode == 17) // Ctrl
3410 this.pressingCtrl_ = false;
3414 * KeyDown event handler for the div#list-container element.
3415 * @param {Event} event Key event.
3418 FileManager.prototype.onListKeyDown_ = function(event) {
3419 if (event.srcElement.tagName == 'INPUT') {
3420 // Ignore keydown handler in the rename input box.
3424 switch (util.getKeyModifiers(event) + event.keyCode) {
3425 case '8': // Backspace => Up one directory.
3426 event.preventDefault();
3427 // TODO(mtomasz): Use Entry.getParent() instead.
3428 if (!this.getCurrentDirectoryEntry())
3430 var currentEntry = this.getCurrentDirectoryEntry();
3431 var locationInfo = this.volumeManager_.getLocationInfo(currentEntry);
3432 // TODO(mtomasz): There may be a tiny race in here.
3433 if (locationInfo && !locationInfo.isRootEntry &&
3434 !locationInfo.isSpecialSearchRoot) {
3435 currentEntry.getParent(function(parentEntry) {
3436 this.directoryModel_.changeDirectoryEntry(parentEntry);
3437 }.bind(this), function() { /* Ignore errors. */});
3441 case '13': // Enter => Change directory or perform default action.
3442 // TODO(dgozman): move directory action to dispatchSelectionAction.
3443 var selection = this.getSelection();
3444 if (selection.totalCount == 1 &&
3445 selection.entries[0].isDirectory &&
3446 !DialogType.isFolderDialog(this.dialogType)) {
3447 event.preventDefault();
3448 this.onDirectoryAction_(selection.entries[0]);
3449 } else if (this.dispatchSelectionAction_()) {
3450 event.preventDefault();
3455 switch (event.keyIdentifier) {
3462 // When navigating with keyboard we hide the distracting mouse hover
3463 // highlighting until the user moves the mouse again.
3464 this.setNoHover_(true);
3470 * Suppress/restore hover highlighting in the list container.
3471 * @param {boolean} on True to temporarity hide hover state.
3474 FileManager.prototype.setNoHover_ = function(on) {
3476 this.listContainer_.classList.add('nohover');
3478 this.listContainer_.classList.remove('nohover');
3483 * KeyPress event handler for the div#list-container element.
3484 * @param {Event} event Key event.
3487 FileManager.prototype.onListKeyPress_ = function(event) {
3488 if (event.srcElement.tagName == 'INPUT') {
3489 // Ignore keypress handler in the rename input box.
3493 if (event.ctrlKey || event.metaKey || event.altKey)
3496 var now = new Date();
3497 var char = String.fromCharCode(event.charCode).toLowerCase();
3498 var text = now - this.textSearchState_.date > 1000 ? '' :
3499 this.textSearchState_.text;
3500 this.textSearchState_ = {text: text + char, date: now};
3502 this.doTextSearch_();
3506 * Mousemove event handler for the div#list-container element.
3507 * @param {Event} event Mouse event.
3510 FileManager.prototype.onListMouseMove_ = function(event) {
3511 // The user grabbed the mouse, restore the hover highlighting.
3512 this.setNoHover_(false);
3516 * Performs a 'text search' - selects a first list entry with name
3517 * starting with entered text (case-insensitive).
3520 FileManager.prototype.doTextSearch_ = function() {
3521 var text = this.textSearchState_.text;
3525 var dm = this.directoryModel_.getFileList();
3526 for (var index = 0; index < dm.length; ++index) {
3527 var name = dm.item(index).name;
3528 if (name.substring(0, text.length).toLowerCase() == text) {
3529 this.currentList_.selectionModel.selectedIndexes = [index];
3534 this.textSearchState_.text = '';
3538 * Handle a click of the cancel button. Closes the window.
3539 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3541 * @param {Event} event The click event.
3544 FileManager.prototype.onCancel_ = function(event) {
3545 chrome.fileManagerPrivate.cancelDialog();
3550 * Tries to close this modal dialog with some files selected.
3551 * Performs preprocessing if needed (e.g. for Drive).
3552 * @param {Object} selection Contains urls, filterIndex and multiple fields.
3555 FileManager.prototype.selectFilesAndClose_ = function(selection) {
3556 var callSelectFilesApiAndClose = function(callback) {
3557 var onFileSelected = function() {
3559 if (!chrome.runtime.lastError) {
3560 // Call next method on a timeout, as it's unsafe to
3561 // close a window from a callback.
3562 setTimeout(window.close.bind(window), 0);
3565 if (selection.multiple) {
3566 chrome.fileManagerPrivate.selectFiles(
3568 this.params_.shouldReturnLocalPath,
3571 chrome.fileManagerPrivate.selectFile(
3573 selection.filterIndex,
3574 this.dialogType != DialogType.SELECT_SAVEAS_FILE /* for opening */,
3575 this.params_.shouldReturnLocalPath,
3580 if (!this.isOnDrive() || this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3581 callSelectFilesApiAndClose(function() {});
3585 var shade = this.document_.createElement('div');
3586 shade.className = 'shade';
3587 var footer = this.dialogDom_.querySelector('.button-panel');
3588 var progress = footer.querySelector('.progress-track');
3589 progress.style.width = '0%';
3590 var cancelled = false;
3592 var progressMap = {};
3593 var filesStarted = 0;
3594 var filesTotal = selection.urls.length;
3595 for (var index = 0; index < selection.urls.length; index++) {
3596 progressMap[selection.urls[index]] = -1;
3598 var lastPercent = 0;
3602 var onFileTransfersUpdated = function(status) {
3603 if (!(status.fileUrl in progressMap))
3605 if (status.total == -1)
3608 var old = progressMap[status.fileUrl];
3610 // -1 means we don't know file size yet.
3611 bytesTotal += status.total;
3615 bytesDone += status.processed - old;
3616 progressMap[status.fileUrl] = status.processed;
3618 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
3619 // For files we don't have information about, assume the progress is zero.
3620 percent = percent * filesStarted / filesTotal * 100;
3621 // Do not decrease the progress. This may happen, if first downloaded
3622 // file is small, and the second one is large.
3623 lastPercent = Math.max(lastPercent, percent);
3624 progress.style.width = lastPercent + '%';
3627 var setup = function() {
3628 this.document_.querySelector('.dialog-container').appendChild(shade);
3629 setTimeout(function() { shade.setAttribute('fadein', 'fadein'); }, 100);
3630 footer.setAttribute('progress', 'progress');
3631 this.cancelButton_.removeEventListener('click', this.onCancelBound_);
3632 this.cancelButton_.addEventListener('click', onCancel);
3633 chrome.fileManagerPrivate.onFileTransfersUpdated.addListener(
3634 onFileTransfersUpdated);
3637 var cleanup = function() {
3638 shade.parentNode.removeChild(shade);
3639 footer.removeAttribute('progress');
3640 this.cancelButton_.removeEventListener('click', onCancel);
3641 this.cancelButton_.addEventListener('click', this.onCancelBound_);
3642 chrome.fileManagerPrivate.onFileTransfersUpdated.removeListener(
3643 onFileTransfersUpdated);
3646 var onCancel = function() {
3647 // According to API cancel may fail, but there is no proper UI to reflect
3648 // this. So, we just silently assume that everything is cancelled.
3649 chrome.fileManagerPrivate.cancelFileTransfers(
3650 selection.urls, function(response) {});
3653 var onProperties = function(properties) {
3654 for (var i = 0; i < properties.length; i++) {
3655 if (!properties[i] || properties[i].present) {
3656 // For files already in GCache, we don't get any transfer updates.
3660 callSelectFilesApiAndClose(cleanup);
3665 // TODO(mtomasz): Use Entry instead of URLs, if possible.
3666 util.URLsToEntries(selection.urls, function(entries) {
3667 this.metadataCache_.get(entries, 'external', onProperties);
3672 * Handle a click of the ok button.
3674 * The ok button has different UI labels depending on the type of dialog, but
3675 * in code it's always referred to as 'ok'.
3677 * @param {Event} event The click event.
3680 FileManager.prototype.onOk_ = function(event) {
3681 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3682 // Save-as doesn't require a valid selection from the list, since
3683 // we're going to take the filename from the text input.
3684 var filename = this.filenameInput_.value;
3686 throw new Error('Missing filename!');
3688 var directory = this.getCurrentDirectoryEntry();
3689 this.validateFileName_(directory, filename, function(isValid) {
3693 if (util.isFakeEntry(directory)) {
3694 // Can't save a file into a fake directory.
3698 var selectFileAndClose = function() {
3699 // TODO(mtomasz): Clean this up by avoiding constructing a URL
3700 // via string concatenation.
3701 var currentDirUrl = directory.toURL();
3702 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
3703 currentDirUrl += '/';
3704 this.selectFilesAndClose_({
3705 urls: [currentDirUrl + encodeURIComponent(filename)],
3707 filterIndex: this.getSelectedFilterIndex_(filename)
3712 filename, {create: false},
3714 // An existing file is found. Show confirmation dialog to
3715 // overwrite it. If the user select "OK" on the dialog, save it.
3716 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
3717 selectFileAndClose);
3720 if (error.name == util.FileError.NOT_FOUND_ERR) {
3721 // The file does not exist, so it should be ok to create a
3723 selectFileAndClose();
3726 if (error.name == util.FileError.TYPE_MISMATCH_ERR) {
3727 // An directory is found.
3728 // Do not allow to overwrite directory.
3729 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
3733 // Unexpected error.
3734 console.error('File save failed: ' + error.code);
3741 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
3743 if (DialogType.isFolderDialog(this.dialogType) &&
3744 selectedIndexes.length == 0) {
3745 var url = this.getCurrentDirectoryEntry().toURL();
3746 var singleSelection = {
3749 filterIndex: this.getSelectedFilterIndex_()
3751 this.selectFilesAndClose_(singleSelection);
3755 // All other dialog types require at least one selected list item.
3756 // The logic to control whether or not the ok button is enabled should
3757 // prevent us from ever getting here, but we sanity check to be sure.
3758 if (!selectedIndexes.length)
3759 throw new Error('Nothing selected!');
3761 var dm = this.directoryModel_.getFileList();
3762 for (var i = 0; i < selectedIndexes.length; i++) {
3763 var entry = dm.item(selectedIndexes[i]);
3765 console.error('Error locating selected file at index: ' + i);
3769 files.push(entry.toURL());
3772 // Multi-file selection has no other restrictions.
3773 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
3774 var multipleSelection = {
3778 this.selectFilesAndClose_(multipleSelection);
3782 // Everything else must have exactly one.
3783 if (files.length > 1)
3784 throw new Error('Too many files selected!');
3786 var selectedEntry = dm.item(selectedIndexes[0]);
3788 if (DialogType.isFolderDialog(this.dialogType)) {
3789 if (!selectedEntry.isDirectory)
3790 throw new Error('Selected entry is not a folder!');
3791 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
3792 if (!selectedEntry.isFile)
3793 throw new Error('Selected entry is not a file!');
3796 var singleSelection = {
3799 filterIndex: this.getSelectedFilterIndex_()
3801 this.selectFilesAndClose_(singleSelection);
3805 * Verifies the user entered name for file or folder to be created or
3806 * renamed to. See also util.validateFileName.
3808 * @param {DirectoryEntry} parentEntry The URL of the parent directory entry.
3809 * @param {string} name New file or folder name.
3810 * @param {function} onDone Function to invoke when user closes the
3811 * warning box or immediatelly if file name is correct. If the name was
3812 * valid it is passed true, and false otherwise.
3815 FileManager.prototype.validateFileName_ = function(
3816 parentEntry, name, onDone) {
3817 var fileNameErrorPromise = util.validateFileName(
3820 this.fileFilter_.isFilterHiddenOn());
3821 fileNameErrorPromise.then(onDone.bind(null, true), function(message) {
3822 this.alert.show(message, onDone.bind(null, false));
3823 }.bind(this)).catch(function(error) {
3824 console.error(error.stack || error);
3829 * Toggle whether mobile data is used for sync.
3831 FileManager.prototype.toggleDriveSyncSettings = function() {
3832 // If checked, the sync is disabled.
3833 var nowCellularDisabled = this.syncButton.hasAttribute('checked');
3834 var changeInfo = {cellularDisabled: !nowCellularDisabled};
3835 chrome.fileManagerPrivate.setPreferences(changeInfo);
3839 * Toggle whether Google Docs files are shown.
3841 FileManager.prototype.toggleDriveHostedSettings = function() {
3842 // If checked, showing drive hosted files is enabled.
3843 var nowHostedFilesEnabled = this.hostedButton.hasAttribute('checked');
3844 var nowHostedFilesDisabled = !nowHostedFilesEnabled;
3846 var changeInfo = {hostedFilesDisabled: !nowHostedFilesDisabled};
3848 var changeInfo = {};
3849 changeInfo['hostedFilesDisabled'] = !nowHostedFilesDisabled;
3850 chrome.fileManagerPrivate.setPreferences(changeInfo);
3854 * Invoked when the search box is changed.
3856 * @param {Event} event The changed event.
3859 FileManager.prototype.onSearchBoxUpdate_ = function(event) {
3860 var searchString = this.searchBox_.value;
3862 if (this.isOnDrive()) {
3863 // When the search text is changed, finishes the search and showes back
3864 // the last directory by passing an empty string to
3865 // {@code DirectoryModel.search()}.
3866 if (this.directoryModel_.isSearching() &&
3867 this.lastSearchQuery_ != searchString) {
3871 // On drive, incremental search is not invoked since we have an auto-
3872 // complete suggestion instead.
3876 this.search_(searchString);
3880 * Handle the search clear button click.
3883 FileManager.prototype.onSearchClearButtonClick_ = function() {
3884 this.ui_.searchBox.clear();
3885 this.onSearchBoxUpdate_();
3889 * Search files and update the list with the search result.
3891 * @param {string} searchString String to be searched with.
3894 FileManager.prototype.search_ = function(searchString) {
3895 var noResultsDiv = this.document_.getElementById('no-search-results');
3897 var reportEmptySearchResults = function() {
3898 if (this.directoryModel_.getFileList().length === 0) {
3899 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
3900 // hence we escapes |searchString| here.
3901 var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
3902 util.htmlEscape(searchString));
3903 noResultsDiv.innerHTML = html;
3904 noResultsDiv.setAttribute('show', 'true');
3906 noResultsDiv.removeAttribute('show');
3909 // If the current location is somewhere in Drive, all files in Drive can
3910 // be listed as search results regardless of current location.
3911 // In this case, showing current location is confusing, so use the Drive
3912 // root "My Drive" as the current location.
3913 var entry = this.getCurrentDirectoryEntry();
3914 var locationInfo = this.volumeManager_.getLocationInfo(entry);
3915 if (locationInfo && locationInfo.isDriveBased) {
3916 var rootEntry = locationInfo.volumeInfo.displayRoot;
3918 this.updateLocationLine_(rootEntry);
3922 var hideNoResultsDiv = function() {
3923 noResultsDiv.removeAttribute('show');
3924 this.updateLocationLine_();
3927 this.doSearch(searchString,
3928 reportEmptySearchResults.bind(this),
3929 hideNoResultsDiv.bind(this));
3933 * Performs search and displays results.
3935 * @param {string} searchString Query that will be searched for.
3936 * @param {function()=} opt_onSearchRescan Function that will be called when
3937 * the search directory is rescanned (i.e. search results are displayed).
3938 * @param {function()=} opt_onClearSearch Function to be called when search
3939 * state gets cleared.
3941 FileManager.prototype.doSearch = function(
3942 searchString, opt_onSearchRescan, opt_onClearSearch) {
3943 var onSearchRescan = opt_onSearchRescan || function() {};
3944 var onClearSearch = opt_onClearSearch || function() {};
3946 this.lastSearchQuery_ = searchString;
3947 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
3951 * Requests autocomplete suggestions for files on Drive.
3952 * Once the suggestions are returned, the autocomplete popup will show up.
3954 * @param {string} query The text to autocomplete from.
3957 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
3958 query = query.trimLeft();
3960 // Only Drive supports auto-compelete
3961 if (!this.isOnDrive())
3964 // Remember the most recent query. If there is an other request in progress,
3965 // then it's result will be discarded and it will call a new request for
3967 this.lastAutocompleteQuery_ = query;
3968 if (this.autocompleteSuggestionsBusy_)
3972 this.autocompleteList_.suggestions = [];
3976 var headerItem = {isHeaderItem: true, searchQuery: query};
3977 if (!this.autocompleteList_.dataModel ||
3978 this.autocompleteList_.dataModel.length == 0)
3979 this.autocompleteList_.suggestions = [headerItem];
3981 // Updates only the head item to prevent a flickering on typing.
3982 this.autocompleteList_.dataModel.splice(0, 1, headerItem);
3984 // The autocomplete list should be resized and repositioned here as the
3985 // search box is resized when it's focused.
3986 this.autocompleteList_.syncWidthAndPositionToInput();
3988 this.autocompleteSuggestionsBusy_ = true;
3990 var searchParams = {
3995 chrome.fileManagerPrivate.searchDriveMetadata(
3997 function(suggestions) {
3998 this.autocompleteSuggestionsBusy_ = false;
4000 // Discard results for previous requests and fire a new search
4001 // for the most recent query.
4002 if (query != this.lastAutocompleteQuery_) {
4003 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
4007 // Keeps the items in the suggestion list.
4008 this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
4013 * Opens the currently selected suggestion item.
4016 FileManager.prototype.openAutocompleteSuggestion_ = function() {
4017 var selectedItem = this.autocompleteList_.selectedItem;
4019 // If the entry is the search item or no entry is selected, just change to
4020 // the search result.
4021 if (!selectedItem || selectedItem.isHeaderItem) {
4022 var query = selectedItem ?
4023 selectedItem.searchQuery : this.searchBox_.value;
4024 this.search_(query);
4028 var entry = selectedItem.entry;
4029 // If the entry is a directory, just change the directory.
4030 if (entry.isDirectory) {
4031 this.onDirectoryAction_(entry);
4035 var entries = [entry];
4038 // To open a file, first get the mime type.
4039 this.metadataCache_.get(entries, 'external', function(props) {
4040 var mimeType = props[0].contentMimeType || '';
4041 var mimeTypes = [mimeType];
4042 var openIt = function() {
4043 if (self.dialogType == DialogType.FULL_PAGE) {
4044 var tasks = new FileTasks(self);
4045 tasks.init(entries, mimeTypes);
4046 tasks.executeDefault();
4052 // Change the current directory to the directory that contains the
4053 // selected file. Note that this is necessary for an image or a video,
4054 // which should be opened in the gallery mode, as the gallery mode
4055 // requires the entry to be in the current directory model. For
4056 // consistency, the current directory is always changed regardless of
4058 entry.getParent(function(parentEntry) {
4059 var onDirectoryChanged = function(event) {
4060 self.directoryModel_.removeEventListener('scan-completed',
4061 onDirectoryChanged);
4062 self.directoryModel_.selectEntry(entry);
4065 // changeDirectoryEntry() returns immediately. We should wait until the
4066 // directory scan is complete.
4067 self.directoryModel_.addEventListener('scan-completed',
4068 onDirectoryChanged);
4069 self.directoryModel_.changeDirectoryEntry(
4072 // Remove the listner if the change directory failed.
4073 self.directoryModel_.removeEventListener('scan-completed',
4074 onDirectoryChanged);
4080 FileManager.prototype.decorateSplitter = function(splitterElement) {
4083 var Splitter = cr.ui.Splitter;
4085 var customSplitter = cr.ui.define('div');
4087 customSplitter.prototype = {
4088 __proto__: Splitter.prototype,
4090 handleSplitterDragStart: function(e) {
4091 Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
4092 this.ownerDocument.documentElement.classList.add('col-resize');
4095 handleSplitterDragMove: function(deltaX) {
4096 Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
4100 handleSplitterDragEnd: function(e) {
4101 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
4102 this.ownerDocument.documentElement.classList.remove('col-resize');
4106 customSplitter.decorate(splitterElement);
4110 * Updates default action menu item to match passed taskItem (icon,
4111 * label and action).
4113 * @param {Object} defaultItem - taskItem to match.
4114 * @param {boolean} isMultiple - if multiple tasks available.
4116 FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
4119 if (defaultItem.iconType) {
4120 this.defaultActionMenuItem_.style.backgroundImage = '';
4121 this.defaultActionMenuItem_.setAttribute('file-type-icon',
4122 defaultItem.iconType);
4123 } else if (defaultItem.iconUrl) {
4124 this.defaultActionMenuItem_.style.backgroundImage =
4125 'url(' + defaultItem.iconUrl + ')';
4127 this.defaultActionMenuItem_.style.backgroundImage = '';
4130 this.defaultActionMenuItem_.label = defaultItem.title;
4131 this.defaultActionMenuItem_.disabled = !!defaultItem.disabled;
4132 this.defaultActionMenuItem_.taskId = defaultItem.taskId;
4135 var defaultActionSeparator =
4136 this.dialogDom_.querySelector('#default-action-separator');
4138 this.openWithCommand_.canExecuteChange();
4139 this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
4140 this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled;
4142 this.defaultActionMenuItem_.hidden = !defaultItem;
4143 defaultActionSeparator.hidden = !defaultItem;
4147 * @return {FileSelection} Selection object.
4149 FileManager.prototype.getSelection = function() {
4150 return this.selectionHandler_.selection;
4154 * @return {ArrayDataModel} File list.
4156 FileManager.prototype.getFileList = function() {
4157 return this.directoryModel_.getFileList();
4161 * @return {cr.ui.List} Current list object.
4163 FileManager.prototype.getCurrentList = function() {
4164 return this.currentList_;
4168 * Retrieve the preferences of the files.app. This method caches the result
4169 * and returns it unless opt_update is true.
4170 * @param {function(Object.<string, *>)} callback Callback to get the
4172 * @param {boolean=} opt_update If is's true, don't use the cache and
4173 * retrieve latest preference. Default is false.
4176 FileManager.prototype.getPreferences_ = function(callback, opt_update) {
4177 if (!opt_update && this.preferences_ !== null) {
4178 callback(this.preferences_);
4182 chrome.fileManagerPrivate.getPreferences(function(prefs) {
4183 this.preferences_ = prefs;