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 this.initializeQueue_ = new AsyncUtil.Group();
24 this.listType_ = null;
27 * Whether to suppress the focus moving or not.
28 * This is used to filter out focusing by mouse.
32 this.suppressFocus_ = false;
36 * @type {SelectionHandler}
39 this.selectionHandler_ = null;
43 * Maximum delay in milliseconds for updating thumbnails in the bottom panel
44 * to mitigate flickering. If images load faster then the delay they replace
45 * old images smoothly. On the other hand we don't want to keep old images
51 FileManager.THUMBNAIL_SHOW_DELAY = 100;
53 FileManager.prototype = {
54 __proto__: cr.EventTarget.prototype,
55 get directoryModel() {
56 return this.directoryModel_;
58 get navigationList() {
59 return this.navigationList_;
62 return this.document_;
64 get fileTransferController() {
65 return this.fileTransferController_;
67 get backgroundPage() {
68 return this.backgroundPage_;
73 * Unload the file manager.
74 * Used by background.js (when running in the packaged mode).
77 fileManager.onBeforeUnload_();
78 fileManager.onUnload_();
82 * List of dialog types.
84 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
85 * FULL_PAGE which is specific to this code.
90 SELECT_FOLDER: 'folder',
91 SELECT_UPLOAD_FOLDER: 'upload-folder',
92 SELECT_SAVEAS_FILE: 'saveas-file',
93 SELECT_OPEN_FILE: 'open-file',
94 SELECT_OPEN_MULTI_FILE: 'open-multi-file',
95 FULL_PAGE: 'full-page'
99 * @param {string} type Dialog type.
100 * @return {boolean} Whether the type is modal.
102 DialogType.isModal = function(type) {
103 return type == DialogType.SELECT_FOLDER ||
104 type == DialogType.SELECT_UPLOAD_FOLDER ||
105 type == DialogType.SELECT_SAVEAS_FILE ||
106 type == DialogType.SELECT_OPEN_FILE ||
107 type == DialogType.SELECT_OPEN_MULTI_FILE;
111 * @param {string} type Dialog type.
112 * @return {boolean} Whether the type is open dialog.
114 DialogType.isOpenDialog = function(type) {
115 return type == DialogType.SELECT_OPEN_FILE ||
116 type == DialogType.SELECT_OPEN_MULTI_FILE;
120 * Bottom margin of the list and tree for transparent preview panel.
123 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
125 // Anonymous "namespace".
128 // Private variables and helper functions.
131 * Number of milliseconds in a day.
133 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
136 * Some UI elements react on a single click and standard double click handling
137 * leads to confusing results. We ignore a second click if it comes soon
140 var DOUBLE_CLICK_TIMEOUT = 200;
143 * Update the element to display the information about remaining space for
145 * @param {!Element} spaceInnerBar Block element for a percentage bar
146 * representing the remaining space.
147 * @param {!Element} spaceInfoLabel Inline element to contain the message.
148 * @param {!Element} spaceOuterBar Block element around the percentage bar.
150 var updateSpaceInfo = function(
151 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
152 spaceInnerBar.removeAttribute('pending');
153 if (sizeStatsResult) {
154 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
155 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
158 sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
159 spaceInnerBar.style.width =
160 (100 * usedSpace / sizeStatsResult.totalSize) + '%';
162 spaceOuterBar.hidden = false;
164 spaceOuterBar.hidden = true;
165 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
171 FileManager.ListType = {
176 FileManager.prototype.initPreferences_ = function(callback) {
177 var group = new AsyncUtil.Group();
179 // DRIVE preferences should be initialized before creating DirectoryModel
180 // to rebuild the roots list.
181 group.add(this.getPreferences_.bind(this));
183 // Get startup preferences.
184 this.viewOptions_ = {};
185 group.add(function(done) {
186 util.platform.getPreference(this.startupPrefName_, function(value) {
187 // Load the global default options.
189 this.viewOptions_ = JSON.parse(value);
191 // Override with window-specific options.
192 if (window.appState && window.appState.viewOptions) {
193 for (var key in window.appState.viewOptions) {
194 if (window.appState.viewOptions.hasOwnProperty(key))
195 this.viewOptions_[key] = window.appState.viewOptions[key];
202 // Get the command line option.
203 group.add(function(done) {
204 chrome.commandLinePrivate.hasSwitch(
205 'file-manager-show-checkboxes', function(flag) {
206 this.showCheckboxes_ = flag;
211 // TODO(yoshiki): Remove the flag when the feature is launched.
212 this.enableExperimentalWebstoreIntegration_ = false;
213 group.add(function(done) {
214 chrome.commandLinePrivate.hasSwitch(
215 'file-manager-enable-webstore-integration', function(flag) {
216 this.enableExperimentalWebstoreIntegration_ = flag;
225 * One time initialization for the file system and related things.
227 * @param {function()} callback Completion callback.
230 FileManager.prototype.initFileSystemUI_ = function(callback) {
231 this.table_.startBatchUpdates();
232 this.grid_.startBatchUpdates();
234 this.initFileList_();
235 this.setupCurrentDirectory_();
237 // PyAuto tests monitor this state by polling this variable
238 this.__defineGetter__('workerInitialized_', function() {
239 return this.metadataCache_.isInitialized();
242 this.initDateTimeFormatters_();
246 // Get the 'allowRedeemOffers' preference before launching
247 // FileListBannerController.
248 this.getPreferences_(function(pref) {
249 /** @type {boolean} */
250 var showOffers = pref['allowRedeemOffers'];
251 self.bannersController_ = new FileListBannerController(
252 self.directoryModel_, self.volumeManager_, self.document_,
254 self.bannersController_.addEventListener('relayout',
255 self.onResize_.bind(self));
258 var dm = this.directoryModel_;
259 dm.addEventListener('directory-changed',
260 this.onDirectoryChanged_.bind(this));
261 dm.addEventListener('begin-update-files', function() {
262 self.currentList_.startBatchUpdates();
264 dm.addEventListener('end-update-files', function() {
265 self.restoreItemBeingRenamed_();
266 self.currentList_.endBatchUpdates();
268 dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
269 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
270 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
271 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
272 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
273 dm.addEventListener('rescan-completed',
274 this.onRescanCompleted_.bind(this));
276 var sm = this.directoryModel_.getFileListSelection();
277 sm.addEventListener('change', function() {
278 if (sm.selectedIndexes.length != 1)
280 var view = (this.listType_ == FileManager.ListType.DETAIL) ?
281 this.table_.list : this.grid_;
282 var selectedItem = view.getListItemByIndex(sm.selectedIndex);
285 this.ensureItemNotBehindPreviewPanel_(selectedItem, view);
288 this.directoryTree_.addEventListener('change', function() {
289 var selectedSubTree = this.directoryTree_.selectedItem;
290 if (!selectedSubTree)
292 var selectedItem = selectedSubTree.rowElement;
293 this.ensureItemNotBehindPreviewPanel_(selectedItem, this.directoryTree_);
296 var stateChangeHandler =
297 this.onPreferencesChanged_.bind(this);
298 chrome.fileBrowserPrivate.onPreferencesChanged.addListener(
300 stateChangeHandler();
302 var driveConnectionChangedHandler =
303 this.onDriveConnectionChanged_.bind(this);
304 this.volumeManager_.addEventListener('drive-connection-changed',
305 driveConnectionChangedHandler);
306 driveConnectionChangedHandler();
308 // Set the initial focus.
310 // Set it as a fallback when there is no focus.
311 this.document_.addEventListener('focusout', function(e) {
312 setTimeout(function() {
313 // When there is no focus, the active element is the <body>.
314 if (this.document_.activeElement == this.document_.body)
319 this.initDataTransferOperations_();
321 this.initContextMenus_();
322 this.initCommands_();
324 this.updateFileTypeFilter_();
326 this.selectionHandler_.onFileSelectionChanged();
328 this.table_.endBatchUpdates();
329 this.grid_.endBatchUpdates();
335 * If |item| in |parentView| is behind the preview panel, scrolls up the
336 * parent view and make the item visible. This should be called when:
337 * - the selected item is changed.
338 * - the visibility of the the preview panel is changed.
340 * @param {HTMLElement} item Item to be visible in the parent.
341 * @param {HTMLElement} parentView View contains |selectedItem|.
344 FileManager.prototype.ensureItemNotBehindPreviewPanel_ =
345 function(item, parentView) {
346 var itemRect = item.getBoundingClientRect();
350 var listRect = parentView.getBoundingClientRect();
354 var previewPanel = this.dialogDom_.querySelector('.preview-panel');
355 var previewPanelRect = previewPanel.getBoundingClientRect();
356 var panelHeight = previewPanelRect ? previewPanelRect.height : 0;
358 var itemBottom = itemRect.bottom;
359 var listBottom = listRect.bottom - panelHeight;
361 if (itemBottom > listBottom) {
362 var scrollOffset = itemBottom - listBottom;
363 parentView.scrollTop += scrollOffset;
370 FileManager.prototype.initDateTimeFormatters_ = function() {
371 var use12hourClock = !this.preferences_['use24hourClock'];
372 this.table_.setDateTimeFormat(use12hourClock);
378 FileManager.prototype.initDataTransferOperations_ = function() {
379 this.fileOperationManager_ = FileOperationManagerWrapper.getInstance(
380 this.backgroundPage_);
382 this.butterBar_ = new ButterBar(
383 this.dialogDom_, this.fileOperationManager_);
385 // CopyManager and ButterBar are required for 'Delete' operation in
386 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
387 if (this.dialogType != DialogType.FULL_PAGE) return;
389 // TODO(hidehiko): Extract FileOperationManager related code from
390 // FileManager to simplify it.
391 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
392 this.fileOperationManager_.addEventListener(
393 'copy-progress', this.onCopyProgressBound_);
395 this.onEntryChangedBound_ = this.onEntryChanged_.bind(this);
396 this.fileOperationManager_.addEventListener(
397 'entry-changed', this.onEntryChangedBound_);
399 var controller = this.fileTransferController_ =
400 new FileTransferController(this.document_,
401 this.fileOperationManager_,
403 this.directoryModel_);
404 controller.attachDragSource(this.table_.list);
405 controller.attachFileListDropTarget(this.table_.list);
406 controller.attachDragSource(this.grid_);
407 controller.attachFileListDropTarget(this.grid_);
408 controller.attachTreeDropTarget(this.directoryTree_);
409 controller.attachNavigationListDropTarget(this.navigationList_, true);
410 controller.attachCopyPasteHandlers();
411 controller.addEventListener('selection-copied',
412 this.blinkSelection.bind(this));
413 controller.addEventListener('selection-cut',
414 this.blinkSelection.bind(this));
418 * One-time initialization of context menus.
421 FileManager.prototype.initContextMenus_ = function() {
422 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
423 cr.ui.Menu.decorate(this.fileContextMenu_);
425 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
426 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
427 this.fileContextMenu_);
428 cr.ui.contextMenuHandler.setContextMenu(
429 this.document_.querySelector('.drive-welcome.page'),
430 this.fileContextMenu_);
432 this.rootsContextMenu_ =
433 this.dialogDom_.querySelector('#roots-context-menu');
434 cr.ui.Menu.decorate(this.rootsContextMenu_);
435 this.navigationList_.setContextMenu(this.rootsContextMenu_);
437 this.directoryTreeContextMenu_ =
438 this.dialogDom_.querySelector('#directory-tree-context-menu');
439 cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
440 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
442 this.textContextMenu_ =
443 this.dialogDom_.querySelector('#text-context-menu');
444 cr.ui.Menu.decorate(this.textContextMenu_);
446 this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
447 this.gearButton_.addEventListener('menushow',
448 this.refreshRemainingSpace_.bind(this,
449 false /* Without loading caption. */));
450 this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
452 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
454 if (this.dialogType == DialogType.FULL_PAGE) {
455 // This is to prevent the buttons from stealing focus on mouse down.
456 var preventFocus = function(event) {
457 event.preventDefault();
460 var maximizeButton = this.dialogDom_.querySelector('#maximize-button');
461 maximizeButton.addEventListener('click', this.onMaximize.bind(this));
462 maximizeButton.addEventListener('mousedown', preventFocus);
464 var closeButton = this.dialogDom_.querySelector('#close-button');
465 closeButton.addEventListener('click', this.onClose.bind(this));
466 closeButton.addEventListener('mousedown', preventFocus);
469 this.syncButton.checkable = true;
470 this.hostedButton.checkable = true;
471 this.detailViewButton_.checkable = true;
472 this.thumbnailViewButton_.checkable = true;
474 if (util.platform.runningInBrowser()) {
475 // Suppresses the default context menu.
476 this.dialogDom_.addEventListener('contextmenu', function(e) {
483 FileManager.prototype.onMaximize = function() {
484 var appWindow = chrome.app.window.current();
485 if (appWindow.isMaximized())
488 appWindow.maximize();
491 FileManager.prototype.onClose = function() {
496 * One-time initialization of commands.
499 FileManager.prototype.initCommands_ = function() {
500 this.commandHandler = new CommandHandler(this);
502 // TODO(hirono): Move the following block to the UI part.
503 var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
504 for (var j = 0; j < commandButtons.length; j++)
505 CommandButton.decorate(commandButtons[j]);
507 var inputs = this.dialogDom_.querySelectorAll(
508 'input[type=text], input[type=search], textarea');
509 for (var i = 0; i < inputs.length; i++) {
510 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
511 this.registerInputCommands_(inputs[i]);
514 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
515 this.textContextMenu_);
516 this.registerInputCommands_(this.renameInput_);
517 this.document_.addEventListener('command',
518 this.setNoHover_.bind(this, true));
522 * Registers cut, copy, paste and delete commands on input element.
524 * @param {Node} node Text input element to register on.
527 FileManager.prototype.registerInputCommands_ = function(node) {
528 CommandUtil.forceDefaultHandler(node, 'cut');
529 CommandUtil.forceDefaultHandler(node, 'copy');
530 CommandUtil.forceDefaultHandler(node, 'paste');
531 CommandUtil.forceDefaultHandler(node, 'delete');
532 node.addEventListener('keydown', function(e) {
533 if (util.getKeyModifiers(e) + e.keyCode == '191') {
534 // If this key event is propagated, this is handled search command,
535 // which calls 'preventDefault' method.
542 * Entry point of the initialization.
543 * This method is called from main.js.
545 FileManager.prototype.initializeCore = function() {
546 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
547 this.initializeQueue_.add(this.initBackgroundPage_.bind(this),
548 [], 'initBackgroundPage');
549 this.initializeQueue_.add(this.initPreferences_.bind(this),
550 ['initGeneral'], 'initPreferences');
551 this.initializeQueue_.add(this.initVolumeManager_.bind(this),
552 ['initGeneral', 'initBackgroundPage'],
553 'initVolumeManager');
555 this.initializeQueue_.run();
558 FileManager.prototype.initializeUI = function(dialogDom, callback) {
559 this.dialogDom_ = dialogDom;
560 this.document_ = this.dialogDom_.ownerDocument;
562 this.initializeQueue_.add(
563 this.initEssentialUI_.bind(this),
564 ['initGeneral', 'initBackgroundPage'],
566 this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
567 ['initEssentialUI'], 'initAdditionalUI');
568 this.initializeQueue_.add(
569 this.initFileSystemUI_.bind(this),
570 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI');
572 // Run again just in case if all pending closures have completed and the
573 // queue has stopped and monitor the completion.
574 this.initializeQueue_.run(callback);
578 * Initializes general purpose basic things, which are used by other
579 * initializing methods.
581 * @param {function()} callback Completion callback.
584 FileManager.prototype.initGeneral_ = function(callback) {
585 // Initialize the application state.
586 if (window.appState) {
587 this.params_ = window.appState.params || {};
588 this.defaultPath = window.appState.defaultPath;
590 this.params_ = location.search ?
591 JSON.parse(decodeURIComponent(location.search.substr(1))) :
593 this.defaultPath = this.params_.defaultPath;
596 // Initialize the member variables that depend this.params_.
597 this.dialogType = this.params_.type || DialogType.FULL_PAGE;
598 this.startupPrefName_ = 'file-manager-' + this.dialogType;
599 this.fileTypes_ = this.params_.typeList || [];
605 * Initialize the background page.
606 * @param {function()} callback Completion callback.
609 FileManager.prototype.initBackgroundPage_ = function(callback) {
610 chrome.runtime.getBackgroundPage(function(backgroundPage) {
611 this.backgroundPage_ = backgroundPage;
612 this.backgroundPage_.background.ready(function() {
613 loadTimeData.data = this.backgroundPage_.background.stringData;
620 * Initializes the VolumeManager instance.
621 * @param {function()} callback Completion callback.
624 FileManager.prototype.initVolumeManager_ = function(callback) {
625 // Auto resolving to local path does not work for folders (e.g., dialog for
626 // loading unpacked extensions).
627 var noLocalPathResolution =
628 this.params_.type == DialogType.SELECT_FOLDER ||
629 this.params_.type == DialogType.SELECT_UPLOAD_FOLDER;
631 // If this condition is false, VolumeManagerWrapper hides all drive
632 // related event and data, even if Drive is enabled on preference.
633 // In other words, even if Drive is disabled on preference but Files.app
634 // should show Drive when it is re-enabled, then the value should be set to
636 // Note that the Drive enabling preference change is listened by
637 // DriveIntegrationService, so here we don't need to take care about it.
639 !noLocalPathResolution || !this.params_.shouldReturnLocalPath;
640 this.volumeManager_ = new VolumeManagerWrapper(
641 driveEnabled, this.backgroundPage_);
646 * One time initialization of the Files.app's essential UI elements. These
647 * elements will be shown to the user. Only visible elements should be
648 * initialized here. Any heavy operation should be avoided. Files.app's
649 * window is shown at the end of this routine.
651 * @param {function()} callback Completion callback.
654 FileManager.prototype.initEssentialUI_ = function(callback) {
655 // Optional list of file types.
656 metrics.recordEnum('Create', this.dialogType,
657 [DialogType.SELECT_FOLDER,
658 DialogType.SELECT_UPLOAD_FOLDER,
659 DialogType.SELECT_SAVEAS_FILE,
660 DialogType.SELECT_OPEN_FILE,
661 DialogType.SELECT_OPEN_MULTI_FILE,
662 DialogType.FULL_PAGE]);
664 // Create the metadata cache.
665 this.metadataCache_ = MetadataCache.createFull();
667 // Create the root view of FileManager.
668 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType);
669 this.fileTypeSelector_ = this.ui_.fileTypeSelector;
670 this.okButton_ = this.ui_.okButton;
671 this.cancelButton_ = this.ui_.cancelButton;
673 // Show the window as soon as the UI pre-initialization is done.
674 if (this.dialogType == DialogType.FULL_PAGE &&
675 !util.platform.runningInBrowser()) {
676 chrome.app.window.current().show();
677 setTimeout(callback, 100); // Wait until the animation is finished.
684 * One-time initialization of dialogs.
687 FileManager.prototype.initDialogs_ = function() {
688 // Initialize the dialog.
689 this.ui_.initDialogs();
690 FileManagerDialogBase.setFileManager(this);
692 // Obtains the dialog instances from FileManagerUI.
693 // TODO(hirono): Remove the properties from the FileManager class.
694 this.error = this.ui_.errorDialog;
695 this.alert = this.ui_.alertDialog;
696 this.confirm = this.ui_.confirmDialog;
697 this.prompt = this.ui_.promptDialog;
698 this.shareDialog_ = this.ui_.shareDialog;
699 this.defaultTaskPicker = this.ui_.defaultTaskPicker;
700 this.suggestAppsDialog = this.ui_.suggestAppsDialog;
704 * One-time initialization of various DOM nodes. Loads the additional DOM
705 * elements visible to the user. Initialize here elements, which are expensive
706 * or hidden in the beginning.
708 * @param {function()} callback Completion callback.
711 FileManager.prototype.initAdditionalUI_ = function(callback) {
713 this.ui_.initAdditionalUI();
715 this.dialogDom_.addEventListener('drop', function(e) {
716 // Prevent opening an URL by dropping it onto the page.
720 this.dialogDom_.addEventListener('click',
721 this.onExternalLinkClick_.bind(this));
722 // Cache nodes we'll be manipulating.
723 var dom = this.dialogDom_;
725 this.filenameInput_ = dom.querySelector('#filename-input-box input');
726 this.taskItems_ = dom.querySelector('#tasks');
728 this.table_ = dom.querySelector('.detail-table');
729 this.grid_ = dom.querySelector('.thumbnail-grid');
730 this.spinner_ = dom.querySelector('#list-container > .spinner-layer');
731 this.showSpinner_(true);
733 // Check the option to hide the selecting checkboxes.
734 this.table_.showCheckboxes = this.showCheckboxes_;
736 var fullPage = this.dialogType == DialogType.FULL_PAGE;
737 FileTable.decorate(this.table_, this.metadataCache_, fullPage);
738 FileGrid.decorate(this.grid_, this.metadataCache_);
740 this.previewPanel_ = new PreviewPanel(
741 dom.querySelector('.preview-panel'),
742 DialogType.isOpenDialog(this.dialogType) ?
743 PreviewPanel.VisibilityType.ALWAYS_VISIBLE :
744 PreviewPanel.VisibilityType.AUTO,
745 this.getCurrentDirectory(),
746 this.metadataCache_);
747 this.previewPanel_.addEventListener(
748 PreviewPanel.Event.VISIBILITY_CHANGE,
749 this.onPreviewPanelVisibilityChange_.bind(this));
750 this.previewPanel_.initialize();
752 this.previewPanel_.breadcrumbs.addEventListener(
753 'pathclick', this.onBreadcrumbClick_.bind(this));
755 // Initialize progress center panel.
756 this.progressCenterPanel_ = new ProgressCenterPanel(
757 dom.querySelector('#progress-center'),
758 this.backgroundPage_.background.progressCenter.requestCancel.bind(
759 this.backgroundPage_.background.progressCenter));
761 this.backgroundPage_.background.progressCenter.applicationItems;
762 for (var i = 0; i < initialItems.length; i++) {
763 this.progressCenterPanel_.updateItem(
765 this.backgroundPage_.background.progressCenter.getSummarizedItem());
767 this.backgroundPage_.background.progressCenter.addEventListener(
768 ProgressCenterEvent.ITEM_UPDATED,
770 this.progressCenterPanel_.updateItem(
772 this.backgroundPage_.background.progressCenter.
773 getSummarizedItem());
775 this.backgroundPage_.background.progressCenter.addEventListener(
776 ProgressCenterEvent.RESET,
778 this.progressCenterPanel_.reset();
781 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
783 // This capturing event is only used to distinguish focusing using
784 // keyboard from focusing using mouse.
785 this.document_.addEventListener('mousedown', function() {
786 this.suppressFocus_ = true;
789 this.renameInput_ = this.document_.createElement('input');
790 this.renameInput_.className = 'rename';
792 this.renameInput_.addEventListener(
793 'keydown', this.onRenameInputKeyDown_.bind(this));
794 this.renameInput_.addEventListener(
795 'blur', this.onRenameInputBlur_.bind(this));
797 this.filenameInput_.addEventListener(
798 'keydown', this.onFilenameInputKeyDown_.bind(this));
799 this.filenameInput_.addEventListener(
800 'focus', this.onFilenameInputFocus_.bind(this));
802 this.listContainer_ = this.dialogDom_.querySelector('#list-container');
803 this.listContainer_.addEventListener(
804 'keydown', this.onListKeyDown_.bind(this));
805 this.listContainer_.addEventListener(
806 'keypress', this.onListKeyPress_.bind(this));
807 this.listContainer_.addEventListener(
808 'mousemove', this.onListMouseMove_.bind(this));
810 this.okButton_.addEventListener('click', this.onOk_.bind(this));
811 this.onCancelBound_ = this.onCancel_.bind(this);
812 this.cancelButton_.addEventListener('click', this.onCancelBound_);
814 this.decorateSplitter(
815 this.dialogDom_.querySelector('#navigation-list-splitter'));
816 this.decorateSplitter(
817 this.dialogDom_.querySelector('#middlebar-splitter'));
819 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
821 this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings');
822 this.syncButton.addEventListener('click', this.onDrivePrefClick_.bind(
823 this, 'cellularDisabled', false /* not inverted */));
825 this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings');
826 this.hostedButton.addEventListener('click', this.onDrivePrefClick_.bind(
827 this, 'hostedFilesDisabled', true /* inverted */));
829 this.detailViewButton_ =
830 this.dialogDom_.querySelector('#detail-view');
831 this.detailViewButton_.addEventListener('click',
832 this.onDetailViewButtonClick_.bind(this));
834 this.thumbnailViewButton_ =
835 this.dialogDom_.querySelector('#thumbnail-view');
836 this.thumbnailViewButton_.addEventListener('click',
837 this.onThumbnailViewButtonClick_.bind(this));
839 cr.ui.ComboButton.decorate(this.taskItems_);
840 this.taskItems_.showMenu = function(shouldSetFocus) {
841 // Prevent the empty menu from opening.
842 if (!this.menu.length)
844 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus);
846 this.taskItems_.addEventListener('select',
847 this.onTaskItemClicked_.bind(this));
849 this.dialogDom_.ownerDocument.defaultView.addEventListener(
850 'resize', this.onResize_.bind(this));
852 this.filePopup_ = null;
854 this.searchBoxWrapper_ = this.ui_.searchBox.element;
855 this.searchBox_ = this.ui_.searchBox.inputElement;
856 this.searchBox_.addEventListener(
857 'input', this.onSearchBoxUpdate_.bind(this));
858 this.ui_.searchBox.clearButton.addEventListener(
859 'click', this.onSearchClearButtonClick_.bind(this));
861 this.lastSearchQuery_ = '';
863 this.autocompleteList_ = this.ui_.searchBox.autocompleteList;
864 this.autocompleteList_.requestSuggestions =
865 this.requestAutocompleteSuggestions_.bind(this);
867 // Instead, open the suggested item when Enter key is pressed or
869 this.autocompleteList_.handleEnterKeydown = function(event) {
870 this.openAutocompleteSuggestion_();
871 this.lastAutocompleteQuery_ = '';
872 this.autocompleteList_.suggestions = [];
874 this.autocompleteList_.addEventListener('mousedown', function(event) {
875 this.openAutocompleteSuggestion_();
876 this.lastAutocompleteQuery_ = '';
877 this.autocompleteList_.suggestions = [];
880 this.defaultActionMenuItem_ =
881 this.dialogDom_.querySelector('#default-action');
883 this.openWithCommand_ =
884 this.dialogDom_.querySelector('#open-with');
886 this.driveBuyMoreStorageCommand_ =
887 this.dialogDom_.querySelector('#drive-buy-more-space');
889 this.defaultActionMenuItem_.addEventListener('click',
890 this.dispatchSelectionAction_.bind(this));
892 this.initFileTypeFilter_();
894 util.addIsFocusedMethod();
896 // Populate the static localized strings.
897 i18nTemplate.process(this.document_, loadTimeData);
899 // Arrange the file list.
900 this.table_.normalizeColumns();
901 this.table_.redraw();
909 FileManager.prototype.onBreadcrumbClick_ = function(event) {
910 this.directoryModel_.changeDirectory(event.path);
914 * Constructs table and grid (heavy operation).
917 FileManager.prototype.initFileList_ = function() {
918 // Always sharing the data model between the detail/thumb views confuses
919 // them. Instead we maintain this bogus data model, and hook it up to the
920 // view that is not in use.
921 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
922 this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
924 var singleSelection =
925 this.dialogType == DialogType.SELECT_OPEN_FILE ||
926 this.dialogType == DialogType.SELECT_FOLDER ||
927 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
928 this.dialogType == DialogType.SELECT_SAVEAS_FILE;
930 var showSpecialSearchRoots =
931 this.dialogType == DialogType.SELECT_OPEN_FILE ||
932 this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE ||
933 this.dialogType == DialogType.FULL_PAGE;
935 this.fileFilter_ = new FileFilter(
937 false /* Don't show dot files by default. */);
939 this.fileWatcher_ = new FileWatcher(this.metadataCache_);
940 this.fileWatcher_.addEventListener(
941 'watcher-metadata-changed',
942 this.onWatcherMetadataChanged_.bind(this));
944 this.directoryModel_ = new DirectoryModel(
950 showSpecialSearchRoots);
952 this.folderShortcutsModel_ = new FolderShortcutsDataModel();
954 this.selectionHandler_ = new FileSelectionHandler(this);
956 var dataModel = this.directoryModel_.getFileList();
958 this.table_.setupCompareFunctions(dataModel);
960 dataModel.addEventListener('permuted',
961 this.updateStartupPrefs_.bind(this));
963 this.directoryModel_.getFileListSelection().addEventListener('change',
964 this.selectionHandler_.onFileSelectionChanged.bind(
965 this.selectionHandler_));
967 this.initList_(this.grid_);
968 this.initList_(this.table_.list);
970 var fileListFocusBound = this.onFileListFocus_.bind(this);
971 var fileListBlurBound = this.onFileListBlur_.bind(this);
973 this.table_.list.addEventListener('focus', fileListFocusBound);
974 this.grid_.addEventListener('focus', fileListFocusBound);
976 this.table_.list.addEventListener('blur', fileListBlurBound);
977 this.grid_.addEventListener('blur', fileListBlurBound);
979 var dragStartBound = this.onDragStart_.bind(this);
980 this.table_.list.addEventListener('dragstart', dragStartBound);
981 this.grid_.addEventListener('dragstart', dragStartBound);
983 var dragEndBound = this.onDragEnd_.bind(this);
984 this.table_.list.addEventListener('dragend', dragEndBound);
985 this.grid_.addEventListener('dragend', dragEndBound);
986 // This event is published by DragSelector because drag end event is not
987 // published at the end of drag selection.
988 this.table_.list.addEventListener('dragselectionend', dragEndBound);
989 this.grid_.addEventListener('dragselectionend', dragEndBound);
991 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
992 // attach the directory model.
993 this.initNavigationList_();
995 this.table_.addEventListener('column-resize-end',
996 this.updateStartupPrefs_.bind(this));
998 // Restore preferences.
999 this.directoryModel_.sortFileList(
1000 this.viewOptions_.sortField || 'modificationTime',
1001 this.viewOptions_.sortDirection || 'desc');
1002 if (this.viewOptions_.columns) {
1003 var cm = this.table_.columnModel;
1004 for (var i = 0; i < cm.totalSize; i++) {
1005 if (this.viewOptions_.columns[i] > 0)
1006 cm.setWidth(i, this.viewOptions_.columns[i]);
1009 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
1011 this.textSearchState_ = {text: '', date: new Date()};
1012 this.closeOnUnmount_ = (this.params_.action == 'auto-open');
1014 if (this.closeOnUnmount_) {
1015 this.volumeManager_.addEventListener('externally-unmounted',
1016 this.onExternallyUnmounted_.bind(this));
1019 // Update metadata to change 'Today' and 'Yesterday' dates.
1020 var today = new Date();
1022 today.setMinutes(0);
1023 today.setSeconds(0);
1024 today.setMilliseconds(0);
1025 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1026 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
1032 FileManager.prototype.initNavigationList_ = function() {
1033 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
1034 DirectoryTree.decorate(this.directoryTree_, this.directoryModel_);
1036 this.navigationList_ = this.dialogDom_.querySelector('#navigation-list');
1037 NavigationList.decorate(this.navigationList_,
1038 this.volumeManager_,
1039 this.directoryModel_);
1040 this.navigationList_.fileManager = this;
1041 this.navigationList_.dataModel = new NavigationListModel(
1042 this.volumeManager_, this.folderShortcutsModel_);
1048 FileManager.prototype.updateMiddleBarVisibility_ = function() {
1049 var entry = this.directoryModel_.getCurrentDirEntry();
1053 var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE);
1055 DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath) &&
1056 driveVolume && !driveVolume.error;
1058 querySelector('.dialog-middlebar-contents').hidden = !visible;
1059 this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible;
1066 FileManager.prototype.updateStartupPrefs_ = function() {
1067 var sortStatus = this.directoryModel_.getFileList().sortStatus;
1069 sortField: sortStatus.field,
1070 sortDirection: sortStatus.direction,
1072 listType: this.listType_
1074 var cm = this.table_.columnModel;
1075 for (var i = 0; i < cm.totalSize; i++) {
1076 prefs.columns.push(cm.getWidth(i));
1078 // Save the global default.
1079 util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs));
1081 // Save the window-specific preference.
1082 if (window.appState) {
1083 window.appState.viewOptions = prefs;
1084 util.saveAppState();
1088 FileManager.prototype.refocus = function() {
1090 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
1091 targetElement = this.filenameInput_;
1093 targetElement = this.currentList_;
1095 // Hack: if the tabIndex is disabled, we can assume a modal dialog is
1096 // shown. Focus to a button on the dialog instead.
1097 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1)
1098 targetElement = document.querySelector('button:not([tabIndex="-1"])');
1101 targetElement.focus();
1105 * File list focus handler. Used to select the top most element on the list
1106 * if nothing was selected.
1110 FileManager.prototype.onFileListFocus_ = function() {
1111 // Do not select default item if focused using mouse.
1112 if (this.suppressFocus_)
1115 var selection = this.getSelection();
1116 if (!selection || selection.totalCount != 0)
1119 this.directoryModel_.selectIndex(0);
1123 * File list blur handler.
1127 FileManager.prototype.onFileListBlur_ = function() {
1128 this.suppressFocus_ = false;
1132 * Index of selected item in the typeList of the dialog params.
1134 * @return {number} 1-based index of selected type or 0 if no type selected.
1137 FileManager.prototype.getSelectedFilterIndex_ = function() {
1138 var index = Number(this.fileTypeSelector_.selectedIndex);
1139 if (index < 0) // Nothing selected.
1141 if (this.params_.includeAllFiles) // Already 1-based.
1143 return index + 1; // Convert to 1-based;
1146 FileManager.prototype.setListType = function(type) {
1147 if (type && type == this.listType_)
1150 this.table_.list.startBatchUpdates();
1151 this.grid_.startBatchUpdates();
1153 // TODO(dzvorygin): style.display and dataModel setting order shouldn't
1154 // cause any UI bugs. Currently, the only right way is first to set display
1155 // style and only then set dataModel.
1157 if (type == FileManager.ListType.DETAIL) {
1158 this.table_.dataModel = this.directoryModel_.getFileList();
1159 this.table_.selectionModel = this.directoryModel_.getFileListSelection();
1160 this.table_.hidden = false;
1161 this.grid_.hidden = true;
1162 this.grid_.selectionModel = this.emptySelectionModel_;
1163 this.grid_.dataModel = this.emptyDataModel_;
1164 this.table_.hidden = false;
1165 /** @type {cr.ui.List} */
1166 this.currentList_ = this.table_.list;
1167 this.detailViewButton_.setAttribute('checked', '');
1168 this.thumbnailViewButton_.removeAttribute('checked');
1169 this.detailViewButton_.setAttribute('disabled', '');
1170 this.thumbnailViewButton_.removeAttribute('disabled');
1171 } else if (type == FileManager.ListType.THUMBNAIL) {
1172 this.grid_.dataModel = this.directoryModel_.getFileList();
1173 this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
1174 this.grid_.hidden = false;
1175 this.table_.hidden = true;
1176 this.table_.selectionModel = this.emptySelectionModel_;
1177 this.table_.dataModel = this.emptyDataModel_;
1178 this.grid_.hidden = false;
1179 /** @type {cr.ui.List} */
1180 this.currentList_ = this.grid_;
1181 this.thumbnailViewButton_.setAttribute('checked', '');
1182 this.detailViewButton_.removeAttribute('checked');
1183 this.thumbnailViewButton_.setAttribute('disabled', '');
1184 this.detailViewButton_.removeAttribute('disabled');
1186 throw new Error('Unknown list type: ' + type);
1189 this.listType_ = type;
1190 this.updateStartupPrefs_();
1193 this.table_.list.endBatchUpdates();
1194 this.grid_.endBatchUpdates();
1198 * Initialize the file list table or grid.
1200 * @param {cr.ui.List} list The list.
1203 FileManager.prototype.initList_ = function(list) {
1204 // Overriding the default role 'list' to 'listbox' for better accessibility
1206 list.setAttribute('role', 'listbox');
1207 list.addEventListener('click', this.onDetailClick_.bind(this));
1208 list.id = 'file-list';
1214 FileManager.prototype.onCopyProgress_ = function(event) {
1215 if (event.reason == 'ERROR' &&
1216 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
1217 event.error.data.toDrive &&
1218 event.error.data.code == FileError.QUOTA_EXCEEDED_ERR) {
1219 this.alert.showHtml(
1220 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
1221 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
1223 event.error.data.sourceFileUrl.split('/').pop()),
1224 urlConstants.GOOGLE_DRIVE_BUY_STORAGE));
1227 // TODO(benchan): Currently, there is no FileWatcher emulation for
1228 // drive::FileSystem, so we need to manually trigger the directory rescan
1229 // after paste operations complete. Remove this once we emulate file
1230 // watching functionalities in drive::FileSystem.
1231 if (this.isOnDrive()) {
1232 if (event.reason == 'SUCCESS' || event.reason == 'ERROR' ||
1233 event.reason == 'CANCELLED') {
1234 this.directoryModel_.rescanLater();
1240 * Handler of file manager operations. Called when an entry has been
1242 * This updates directory model to reflect operation result immediately (not
1243 * waiting for directory update event). Also, preloads thumbnails for the
1244 * images of new entries.
1245 * See also FileOperationManager.EventRouter.
1247 * @param {Event} event An event for the entry change.
1250 FileManager.prototype.onEntryChanged_ = function(event) {
1251 var kind = event.kind;
1252 var entry = event.entry;
1253 this.directoryModel_.onEntryChanged(kind, entry);
1254 this.selectionHandler_.onFileSelectionChanged();
1256 if (kind == util.EntryChangedKind.CREATE && FileType.isImage(entry)) {
1257 // Preload a thumbnail if the new copied entry an image.
1258 var metadata = entry.getMetadata(function(metadata) {
1259 var url = entry.toURL();
1260 var thumbnailLoader_ = new ThumbnailLoader(
1262 ThumbnailLoader.LoaderType.CANVAS,
1264 undefined, // Media type.
1265 FileType.isOnDrive(url) ?
1266 ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1267 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1268 10); // Very low priority.
1269 thumbnailLoader_.loadDetachedImage(function(success) {});
1275 * Fills the file type list or hides it.
1278 FileManager.prototype.initFileTypeFilter_ = function() {
1279 if (this.params_.includeAllFiles) {
1280 var option = this.document_.createElement('option');
1281 option.innerText = str('ALL_FILES_FILTER');
1282 this.fileTypeSelector_.appendChild(option);
1286 for (var i = 0; i < this.fileTypes_.length; i++) {
1287 var fileType = this.fileTypes_[i];
1288 var option = this.document_.createElement('option');
1289 var description = fileType.description;
1291 // See if all the extensions in the group have the same description.
1292 for (var j = 0; j != fileType.extensions.length; j++) {
1293 var currentDescription =
1294 FileType.getTypeString('.' + fileType.extensions[j]);
1295 if (!description) // Set the first time.
1296 description = currentDescription;
1297 else if (description != currentDescription) {
1298 // No single description, fall through to the extension list.
1305 // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1306 description = fileType.extensions.map(function(s) {
1310 option.innerText = description;
1312 option.value = i + 1;
1314 if (fileType.selected)
1315 option.selected = true;
1317 this.fileTypeSelector_.appendChild(option);
1320 var options = this.fileTypeSelector_.querySelectorAll('option');
1321 if (options.length < 2) {
1322 // There is in fact no choice, hide the selector.
1323 this.fileTypeSelector_.hidden = true;
1327 this.fileTypeSelector_.addEventListener('change',
1328 this.updateFileTypeFilter_.bind(this));
1332 * Filters file according to the selected file type.
1335 FileManager.prototype.updateFileTypeFilter_ = function() {
1336 this.fileFilter_.removeFilter('fileType');
1337 var selectedIndex = this.getSelectedFilterIndex_();
1338 if (selectedIndex > 0) { // Specific filter selected.
1339 var regexp = new RegExp('.*(' +
1340 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
1341 var filter = function(entry) {
1342 return entry.isDirectory || regexp.test(entry.name);
1344 this.fileFilter_.addFilter('fileType', filter);
1349 * Resize details and thumb views to fit the new window size.
1352 FileManager.prototype.onResize_ = function() {
1353 if (this.listType_ == FileManager.ListType.THUMBNAIL)
1354 this.grid_.relayout();
1356 this.table_.relayout();
1358 // May not be available during initialization.
1359 if (this.directoryTree_)
1360 this.directoryTree_.relayout();
1362 // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before
1363 // file system is available.
1364 if (this.navigationList_)
1365 this.navigationList_.redraw();
1367 this.ui_.searchBox.updateSizeRelatedStyle();
1369 this.previewPanel_.breadcrumbs.truncate();
1373 * Handles local metadata changes in the currect directory.
1374 * @param {Event} event Change event.
1377 FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
1378 this.updateMetadataInUI_(event.metadataType, event.urls, event.properties);
1382 * Resize details and thumb views to fit the new window size.
1385 FileManager.prototype.onPreviewPanelVisibilityChange_ = function() {
1386 // This method may be called on initialization. Some object may not be
1389 var panelHeight = this.previewPanel_.visible ?
1390 this.previewPanel_.height : 0;
1392 this.grid_.setBottomMarginForPanel(panelHeight);
1394 this.table_.setBottomMarginForPanel(panelHeight);
1395 if (this.directoryTree_)
1396 this.directoryTree_.setBottomMarginForPanel(panelHeight);
1398 // Make sure that the selected item is not behind the preview panel.
1399 if (this.directoryModel_) {
1400 var sm = this.directoryModel_.getFileListSelection();
1401 var view = (this.listType_ == FileManager.ListType.DETAIL) ?
1402 this.table_.list : this.grid_;
1403 var selectedItem = view.getListItemByIndex(sm.selectedIndex);
1406 this.ensureItemNotBehindPreviewPanel_(selectedItem, view);
1411 * Invoked when the drag is started on the list or the grid.
1414 FileManager.prototype.onDragStart_ = function() {
1415 // On open file dialog, the preview panel is always shown.
1416 if (DialogType.isOpenDialog(this.dialogType))
1418 this.previewPanel_.visibilityType =
1419 PreviewPanel.VisibilityType.ALWAYS_HIDDEN;
1423 * Invoked when the drag is ended on the list or the grid.
1426 FileManager.prototype.onDragEnd_ = function() {
1427 // On open file dialog, the preview panel is always shown.
1428 if (DialogType.isOpenDialog(this.dialogType))
1430 this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO;
1434 * Restores current directory and may be a selected item after page load (or
1435 * reload) or popping a state (after click on back/forward). If location.hash
1436 * is present it means that the user has navigated somewhere and that place
1437 * will be restored. defaultPath primarily is used with save/open dialogs.
1438 * Default path may also contain a file name. Freshly opened file manager
1439 * window has neither.
1443 FileManager.prototype.setupCurrentDirectory_ = function() {
1444 var path = location.hash ? // Location hash has the highest priority.
1445 decodeURIComponent(location.hash.substr(1)) :
1449 path = PathUtil.DEFAULT_DIRECTORY;
1450 } else if (path.indexOf('/') == -1) {
1451 // Path is a file name.
1452 path = PathUtil.DEFAULT_DIRECTORY + '/' + path;
1455 var tracker = this.directoryModel_.createDirectoryChangeTracker();
1457 this.volumeManager_.ensureInitialized(function() {
1459 if (tracker.hasChanged)
1462 // If Drive is disabled but the path points to Drive's entry,
1463 // fallback to DEFAULT_DIRECTORY.
1464 if (PathUtil.isDriveBasedPath(path) &&
1465 !this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE))
1466 path = PathUtil.DEFAULT_DIRECTORY + '/' + PathUtil.basename(path);
1468 this.finishSetupCurrentDirectory_(path);
1473 * @param {string} path Path to setup.
1476 FileManager.prototype.finishSetupCurrentDirectory_ = function(path) {
1477 this.directoryModel_.setupPath(path, function(baseName, leafName, exists) {
1478 if (this.dialogType == DialogType.FULL_PAGE) {
1479 // In the FULL_PAGE mode if the hash path points to a file we might have
1480 // to invoke a task after selecting it.
1481 // If the file path is in params_ we only want to select the file.
1482 if (this.params_.action == 'select')
1486 if (!exists || leafName == '') {
1487 // Non-existent file or a directory.
1488 if (this.params_.gallery) {
1489 // Reloading while the Gallery is open with empty or multiple
1490 // selection. Open the Gallery when the directory is scanned.
1492 new FileTasks(this, this.params_).openGallery([]);
1496 // There are 3 ways we can get here:
1497 // 1. Invoked from file_manager_util::ViewFile. This can only
1498 // happen for 'gallery' and 'mount-archive' actions.
1499 // 2. Reloading a Gallery page. Must be an image or a video file.
1500 // 3. A user manually entered a URL pointing to a file.
1501 // We call the appropriate methods of FileTasks directly as we do
1502 // not need any of the preparations that |execute| method does.
1503 var mediaType = FileType.getMediaType(path);
1504 if (mediaType == 'image' || mediaType == 'video') {
1506 new FileTasks(this, this.params_).openGallery(
1507 [util.makeFilesystemUrl(path)]);
1509 } else if (mediaType == 'archive') {
1511 new FileTasks(this, this.params_).mountArchives(
1512 [util.makeFilesystemUrl(path)]);
1517 // If there is a task to be run, run it after the scan is completed.
1519 var listener = function() {
1520 this.directoryModel_.removeEventListener(
1521 'scan-completed', listener);
1524 this.directoryModel_.addEventListener('scan-completed', listener);
1529 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
1530 this.filenameInput_.value = leafName;
1531 this.selectDefaultPathInFilenameInput_();
1539 * @param {string} path Path to a volume to unmount.
1541 FileManager.prototype.unmountVolume = function(path) {
1542 var onError = function(error) {
1543 this.alert.showHtml('', str('UNMOUNT_FAILED'));
1545 this.volumeManager_.unmount(path, function() {}, onError.bind(this));
1551 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
1552 var entries = this.directoryModel_.getFileList().slice();
1553 var directoryEntry = this.directoryModel_.getCurrentDirEntry();
1554 if (!directoryEntry)
1556 // We don't pass callback here. When new metadata arrives, we have an
1557 // observer registered to update the UI.
1559 // TODO(dgozman): refresh content metadata only when modificationTime
1561 var isFakeEntry = typeof directoryEntry.toURL !== 'function';
1562 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
1563 this.metadataCache_.clearRecursively(directoryEntry, '*');
1564 this.metadataCache_.get(getEntries, 'filesystem', null);
1566 if (this.isOnDrive())
1567 this.metadataCache_.get(getEntries, 'drive', null);
1569 var visibleItems = this.currentList_.items;
1570 var visibleEntries = [];
1571 for (var i = 0; i < visibleItems.length; i++) {
1572 var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
1573 var entry = this.directoryModel_.getFileList().item(index);
1574 // The following check is a workaround for the bug in list: sometimes item
1575 // does not have listIndex, and therefore is not found in the list.
1576 if (entry) visibleEntries.push(entry);
1578 this.metadataCache_.get(visibleEntries, 'thumbnail', null);
1584 FileManager.prototype.dailyUpdateModificationTime_ = function() {
1585 var fileList = this.directoryModel_.getFileList();
1587 for (var i = 0; i < fileList.length; i++) {
1588 urls.push(fileList.item(i).toURL());
1590 this.metadataCache_.get(
1591 fileList.slice(), 'filesystem',
1592 this.updateMetadataInUI_.bind(this, 'filesystem', urls));
1594 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1595 MILLISECONDS_IN_DAY);
1599 * @param {string} type Type of metadata changed.
1600 * @param {Array.<string>} urls Array of urls.
1601 * @param {Object.<string, Object>} props Map from entry URLs to metadata
1605 FileManager.prototype.updateMetadataInUI_ = function(
1606 type, urls, properties) {
1607 var propertyByUrl = urls.reduce(function(map, url, index) {
1608 map[url] = properties[index];
1612 if (this.listType_ == FileManager.ListType.DETAIL)
1613 this.table_.updateListItemsMetadata(type, propertyByUrl);
1615 this.grid_.updateListItemsMetadata(type, propertyByUrl);
1616 // TODO: update bottom panel thumbnails.
1620 * Restore the item which is being renamed while refreshing the file list. Do
1621 * nothing if no item is being renamed or such an item disappeared.
1623 * While refreshing file list it gets repopulated with new file entries.
1624 * There is not a big difference whether DOM items stay the same or not.
1625 * Except for the item that the user is renaming.
1629 FileManager.prototype.restoreItemBeingRenamed_ = function() {
1630 if (!this.isRenamingInProgress())
1633 var dm = this.directoryModel_;
1634 var leadIndex = dm.getFileListSelection().leadIndex;
1638 var leadEntry = dm.getFileList().item(leadIndex);
1639 if (this.renameInput_.currentEntry.fullPath != leadEntry.fullPath)
1642 var leadListItem = this.findListItemForNode_(this.renameInput_);
1643 if (this.currentList_ == this.table_.list) {
1644 this.table_.updateFileMetadata(leadListItem, leadEntry);
1646 this.currentList_.restoreLeadItem(leadListItem);
1650 * @return {boolean} True if the current directory content is from Google
1653 FileManager.prototype.isOnDrive = function() {
1654 var rootType = this.directoryModel_.getCurrentRootType();
1655 return rootType === RootType.DRIVE ||
1656 rootType === RootType.DRIVE_SHARED_WITH_ME ||
1657 rootType === RootType.DRIVE_RECENT ||
1658 rootType === RootType.DRIVE_OFFLINE;
1662 * Overrides default handling for clicks on hyperlinks.
1663 * In a packaged apps links with targer='_blank' open in a new tab by
1664 * default, other links do not open at all.
1666 * @param {Event} event Click event.
1669 FileManager.prototype.onExternalLinkClick_ = function(event) {
1670 if (event.target.tagName != 'A' || !event.target.href)
1673 if (this.dialogType != DialogType.FULL_PAGE)
1678 * Task combobox handler.
1680 * @param {Object} event Event containing task which was clicked.
1683 FileManager.prototype.onTaskItemClicked_ = function(event) {
1684 var selection = this.getSelection();
1685 if (!selection.tasks) return;
1687 if (event.item.task) {
1688 // Task field doesn't exist on change-default dropdown item.
1689 selection.tasks.execute(event.item.task.taskId);
1691 var extensions = [];
1693 for (var i = 0; i < selection.urls.length; i++) {
1694 var match = /\.(\w+)$/g.exec(selection.urls[i]);
1696 var ext = match[1].toUpperCase();
1697 if (extensions.indexOf(ext) == -1) {
1698 extensions.push(ext);
1705 if (extensions.length == 1) {
1706 format = extensions[0];
1709 // Change default was clicked. We should open "change default" dialog.
1710 selection.tasks.showTaskPicker(this.defaultTaskPicker,
1711 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
1712 strf('CHANGE_DEFAULT_CAPTION', format),
1713 this.onDefaultTaskDone_.bind(this));
1719 * Sets the given task as default, when this task is applicable.
1721 * @param {Object} task Task to set as default.
1724 FileManager.prototype.onDefaultTaskDone_ = function(task) {
1725 // TODO(dgozman): move this method closer to tasks.
1726 var selection = this.getSelection();
1727 chrome.fileBrowserPrivate.setDefaultTask(task.taskId,
1728 selection.urls, selection.mimeTypes);
1729 selection.tasks = new FileTasks(this);
1730 selection.tasks.init(selection.urls, selection.mimeTypes);
1731 selection.tasks.display(this.taskItems_);
1732 this.refreshCurrentDirectoryMetadata_();
1733 this.selectionHandler_.onFileSelectionChanged();
1739 FileManager.prototype.onPreferencesChanged_ = function() {
1741 this.getPreferences_(function(prefs) {
1742 self.initDateTimeFormatters_();
1743 self.refreshCurrentDirectoryMetadata_();
1745 if (prefs.cellularDisabled)
1746 self.syncButton.setAttribute('checked', '');
1748 self.syncButton.removeAttribute('checked');
1750 if (self.hostedButton.hasAttribute('checked') !=
1751 prefs.hostedFilesDisabled && self.isOnDrive()) {
1752 self.directoryModel_.rescan();
1755 if (!prefs.hostedFilesDisabled)
1756 self.hostedButton.setAttribute('checked', '');
1758 self.hostedButton.removeAttribute('checked');
1760 true /* refresh */);
1763 FileManager.prototype.onDriveConnectionChanged_ = function() {
1764 var connection = this.volumeManager_.getDriveConnectionState();
1765 if (this.commandHandler)
1766 this.commandHandler.updateAvailability();
1767 if (this.dialogContainer_)
1768 this.dialogContainer_.setAttribute('connection', connection.type);
1769 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
1770 this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
1774 * Get the metered status of Drive connection.
1776 * @return {boolean} Returns true if drive should limit the traffic because
1777 * the connection is metered and the 'disable-sync-on-metered' setting is
1778 * enabled. Otherwise, returns false.
1780 FileManager.prototype.isDriveOnMeteredConnection = function() {
1781 var connection = this.volumeManager_.getDriveConnectionState();
1782 return connection.type == util.DriveConnectionType.METERED;
1786 * Get the online/offline status of drive.
1788 * @return {boolean} Returns true if the connection is offline. Otherwise,
1791 FileManager.prototype.isDriveOffline = function() {
1792 var connection = this.volumeManager_.getDriveConnectionState();
1793 return connection.type == util.DriveConnectionType.OFFLINE;
1796 FileManager.prototype.isOnReadonlyDirectory = function() {
1797 return this.directoryModel_.isReadOnly();
1801 * @param {Event} Unmount event.
1804 FileManager.prototype.onExternallyUnmounted_ = function(event) {
1805 if (event.mountPath == this.directoryModel_.getCurrentRootPath()) {
1806 if (this.closeOnUnmount_) {
1807 // If the file manager opened automatically when a usb drive inserted,
1808 // user have never changed current volume (that implies the current
1809 // directory is still on the device) then close this window.
1816 * Show a modal-like file viewer/editor on top of the File Manager UI.
1818 * @param {HTMLElement} popup Popup element.
1819 * @param {function()} closeCallback Function to call after the popup is
1822 FileManager.prototype.openFilePopup = function(popup, closeCallback) {
1823 this.closeFilePopup();
1824 this.filePopup_ = popup;
1825 this.filePopupCloseCallback_ = closeCallback;
1826 this.dialogDom_.insertBefore(
1827 this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area'));
1828 this.filePopup_.focus();
1829 this.document_.querySelector('#iframe-drag-area').hidden = false;
1833 * Closes the modal-like file viewer/editor popup.
1835 FileManager.prototype.closeFilePopup = function() {
1836 if (this.filePopup_) {
1837 this.document_.body.removeAttribute('overlay-visible');
1838 this.document_.querySelector('#iframe-drag-area').hidden = true;
1839 // The window resize would not be processed properly while the relevant
1840 // divs had 'display:none', force resize after the layout fired.
1841 setTimeout(this.onResize_.bind(this), 0);
1842 if (this.filePopup_.contentWindow &&
1843 this.filePopup_.contentWindow.unload) {
1844 this.filePopup_.contentWindow.unload();
1847 if (this.filePopupCloseCallback_) {
1848 this.filePopupCloseCallback_();
1849 this.filePopupCloseCallback_ = null;
1852 // These operations have to be in the end, otherwise v8 crashes on an
1853 // assert. See: crbug.com/224174.
1854 this.dialogDom_.removeChild(this.filePopup_);
1855 this.filePopup_ = null;
1860 * Updates visibility of the draggable app region in the modal-like file
1863 * @param {boolean} visible True for visible, false otherwise.
1865 FileManager.prototype.onFilePopupAppRegionChanged = function(visible) {
1866 if (!this.filePopup_)
1869 this.document_.querySelector('#iframe-drag-area').hidden = !visible;
1872 FileManager.prototype.getAllUrlsInCurrentDirectory = function() {
1874 var fileList = this.directoryModel_.getFileList();
1875 for (var i = 0; i != fileList.length; i++) {
1876 urls.push(fileList.item(i).toURL());
1881 FileManager.prototype.isRenamingInProgress = function() {
1882 return !!this.renameInput_.currentEntry;
1888 FileManager.prototype.focusCurrentList_ = function() {
1889 if (this.listType_ == FileManager.ListType.DETAIL)
1890 this.table_.focus();
1891 else // this.listType_ == FileManager.ListType.THUMBNAIL)
1896 * Return full path of the current directory or null.
1897 * @return {?string} The full path of the current directory.
1899 FileManager.prototype.getCurrentDirectory = function() {
1900 return this.directoryModel_ && this.directoryModel_.getCurrentDirPath();
1904 * Return URL of the current directory or null.
1905 * @return {string} URL representing the current directory.
1907 FileManager.prototype.getCurrentDirectoryURL = function() {
1908 return this.directoryModel_ &&
1909 this.directoryModel_.getCurrentDirectoryURL();
1913 * Return DirectoryEntry of the current directory or null.
1914 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
1915 * null if the directory model is not ready or the current directory is
1918 FileManager.prototype.getCurrentDirectoryEntry = function() {
1919 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
1923 * Deletes the selected file and directories recursively.
1925 FileManager.prototype.deleteSelection = function() {
1926 // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364
1927 var entries = this.getSelection().entries;
1928 var message = entries.length == 1 ?
1929 strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) :
1930 strf('GALLERY_CONFIRM_DELETE_SOME', entries.length);
1931 this.confirm.show(message, function() {
1932 this.fileOperationManager_.deleteEntries(entries);
1937 * Shows the share dialog for the selected file or directory.
1939 FileManager.prototype.shareSelection = function() {
1940 var entries = this.getSelection().entries;
1941 if (entries.length != 1) {
1942 console.warn('Unable to share multiple items at once.');
1945 // Add the overlapped class to prevent the applicaiton window from
1946 // captureing mouse events.
1947 this.shareDialog_.show(entries[0], function(result) {
1948 if (result == ShareDialog.Result.NETWORK_ERROR)
1949 this.error.show(str('SHARE_ERROR'));
1954 * Creates a folder shortcut.
1955 * @param {string} path A shortcut which refers to |path| to be created.
1957 FileManager.prototype.createFolderShortcut = function(path) {
1959 if (this.folderShortcutExists(path))
1962 this.folderShortcutsModel_.add(path);
1966 * Checkes if the shortcut which refers to the given folder exists or not.
1967 * @param {string} path Path of the folder to be checked.
1969 FileManager.prototype.folderShortcutExists = function(path) {
1970 return this.folderShortcutsModel_.exists(path);
1974 * Removes the folder shortcut.
1975 * @param {string} path The shortcut which refers to |path| is to be removed.
1977 FileManager.prototype.removeFolderShortcut = function(path) {
1978 this.folderShortcutsModel_.remove(path);
1982 * Blinks the selection. Used to give feedback when copying or cutting the
1985 FileManager.prototype.blinkSelection = function() {
1986 var selection = this.getSelection();
1987 if (!selection || selection.totalCount == 0)
1990 for (var i = 0; i < selection.entries.length; i++) {
1991 var selectedIndex = selection.indexes[i];
1992 var listItem = this.currentList_.getListItemByIndex(selectedIndex);
1994 this.blinkListItem_(listItem);
1999 * @param {Element} listItem List item element.
2002 FileManager.prototype.blinkListItem_ = function(listItem) {
2003 listItem.classList.add('blink');
2004 setTimeout(function() {
2005 listItem.classList.remove('blink');
2012 FileManager.prototype.selectDefaultPathInFilenameInput_ = function() {
2013 var input = this.filenameInput_;
2015 var selectionEnd = input.value.lastIndexOf('.');
2016 if (selectionEnd == -1) {
2019 input.selectionStart = 0;
2020 input.selectionEnd = selectionEnd;
2022 // Clear, so we never do this again.
2023 this.defaultPath = '';
2027 * Handles mouse click or tap.
2029 * @param {Event} event The click event.
2032 FileManager.prototype.onDetailClick_ = function(event) {
2033 if (this.isRenamingInProgress()) {
2034 // Don't pay attention to clicks during a rename.
2038 var listItem = this.findListItemForEvent_(event);
2039 var selection = this.getSelection();
2040 if (!listItem || !listItem.selected || selection.totalCount != 1) {
2044 // React on double click, but only if both clicks hit the same item.
2045 // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2046 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
2047 this.lastClickedItem_ = listItem;
2049 if (event.detail != clickNumber)
2052 var entry = selection.entries[0];
2053 if (entry.isDirectory) {
2054 this.onDirectoryAction_(entry);
2056 this.dispatchSelectionAction_();
2063 FileManager.prototype.dispatchSelectionAction_ = function() {
2064 if (this.dialogType == DialogType.FULL_PAGE) {
2065 var selection = this.getSelection();
2066 var tasks = selection.tasks;
2067 var urls = selection.urls;
2068 var mimeTypes = selection.mimeTypes;
2070 tasks.executeDefault();
2073 if (!this.okButton_.disabled) {
2081 * Opens the suggest file dialog.
2083 * @param {string} url URL of files.
2084 * @param {function()} onSuccess Success callback.
2085 * @param {function()} onCancelled User-cancelled callback.
2086 * @param {function()} onFailure Failure callback.
2089 FileManager.prototype.openSuggestAppsDialog =
2090 function(url, onSuccess, onCancelled, onFailure) {
2096 this.metadataCache_.get([url], 'drive', function(props) {
2097 if (!props || !props[0] || !props[0].contentMimeType) {
2102 var path = util.extractFilePath(url);
2103 var basename = PathUtil.basename(path);
2104 var splitted = PathUtil.splitExtension(basename);
2105 var filename = splitted[0];
2106 var extension = splitted[1];
2107 var mime = props[0].contentMimeType;
2109 // Returns with failure if the file has neither extension nor mime.
2110 if (!extension || !mime) {
2115 var onDialogClosed = function(result) {
2117 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
2120 case SuggestAppsDialog.Result.FAILED:
2128 if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
2129 this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
2131 this.suggestAppsDialog.showByExtensionAndMime(
2132 extension, mime, onDialogClosed);
2138 * Called when a dialog is shown or hidden.
2139 * @param {boolean} flag True if a dialog is shown, false if hidden. */
2140 FileManager.prototype.onDialogShownOrHidden = function(show) {
2141 // Set/unset a flag to disable dragging on the title area.
2142 this.dialogContainer_.classList.toggle('disable-header-drag', show);
2146 * Executes directory action (i.e. changes directory).
2148 * @param {DirectoryEntry} entry Directory entry to which directory should be
2152 FileManager.prototype.onDirectoryAction_ = function(entry) {
2153 return this.directoryModel_.changeDirectory(entry.fullPath);
2157 * Update the window title.
2160 FileManager.prototype.updateTitle_ = function() {
2161 if (this.dialogType != DialogType.FULL_PAGE)
2164 var path = this.getCurrentDirectory();
2165 var rootPath = PathUtil.getRootPath(path);
2166 this.document_.title = PathUtil.getRootLabel(rootPath) +
2167 path.substring(rootPath.length);
2171 * Update the gear menu.
2174 FileManager.prototype.updateGearMenu_ = function() {
2175 var hideItemsForDrive = !this.isOnDrive();
2176 this.syncButton.hidden = hideItemsForDrive;
2177 this.hostedButton.hidden = hideItemsForDrive;
2178 this.document_.getElementById('drive-separator').hidden =
2181 // If volume has changed, then fetch remaining space data.
2182 if (this.previousRootUrl_ != this.directoryModel_.getCurrentMountPointUrl())
2183 this.refreshRemainingSpace_(true); // Show loading caption.
2185 this.previousRootUrl_ = this.directoryModel_.getCurrentMountPointUrl();
2189 * Refreshes space info of the current volume.
2190 * @param {boolean} showLoadingCaption Whether show loading caption or not.
2193 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
2194 var volumeSpaceInfoLabel =
2195 this.dialogDom_.querySelector('#volume-space-info-label');
2196 var volumeSpaceInnerBar =
2197 this.dialogDom_.querySelector('#volume-space-info-bar');
2198 var volumeSpaceOuterBar =
2199 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
2201 volumeSpaceInnerBar.setAttribute('pending', '');
2203 if (showLoadingCaption) {
2204 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2205 volumeSpaceInnerBar.style.width = '100%';
2208 var currentMountPointUrl = this.directoryModel_.getCurrentMountPointUrl();
2209 chrome.fileBrowserPrivate.getSizeStats(
2210 currentMountPointUrl, function(result) {
2211 if (this.directoryModel_.getCurrentMountPointUrl() !=
2212 currentMountPointUrl)
2214 updateSpaceInfo(result,
2215 volumeSpaceInnerBar,
2216 volumeSpaceInfoLabel,
2217 volumeSpaceOuterBar);
2222 * Update the UI when the current directory changes.
2224 * @param {Event} event The directory-changed event.
2227 FileManager.prototype.onDirectoryChanged_ = function(event) {
2228 this.selectionHandler_.onFileSelectionChanged();
2229 this.ui_.searchBox.clear();
2230 util.updateAppState(this.getCurrentDirectory());
2232 // If the current directory is moved from the device's volume, do not
2233 // automatically close the window on device removal.
2234 if (event.previousDirEntry &&
2235 PathUtil.getRootPath(event.previousDirEntry.fullPath) !=
2236 PathUtil.getRootPath(event.newDirEntry.fullPath))
2237 this.closeOnUnmount_ = false;
2239 if (this.commandHandler)
2240 this.commandHandler.updateAvailability();
2241 this.updateUnformattedDriveStatus_();
2242 this.updateTitle_();
2243 this.updateGearMenu_();
2244 this.previewPanel_.currentPath_ = this.getCurrentDirectory();
2247 // TODO(haruki): Rename this method. "Drive" here does not refer
2249 FileManager.prototype.updateUnformattedDriveStatus_ = function() {
2250 var volumeInfo = this.volumeManager_.getVolumeInfo(
2251 PathUtil.getRootPath(this.directoryModel_.getCurrentRootPath()));
2253 if (volumeInfo && volumeInfo.error) {
2254 this.dialogDom_.setAttribute('unformatted', '');
2256 var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2257 if (volumeInfo.error == util.VolumeError.UNSUPPORTED_FILESYSTEM) {
2258 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2260 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2263 // Update 'canExecute' for format command so the format button's disabled
2264 // property is properly set.
2265 if (this.commandHandler)
2266 this.commandHandler.updateAvailability();
2268 this.dialogDom_.removeAttribute('unformatted');
2272 FileManager.prototype.findListItemForEvent_ = function(event) {
2273 return this.findListItemForNode_(event.touchedElement || event.srcElement);
2276 FileManager.prototype.findListItemForNode_ = function(node) {
2277 var item = this.currentList_.getListItemAncestor(node);
2278 // TODO(serya): list should check that.
2279 return item && this.currentList_.isItem(item) ? item : null;
2283 * Unload handler for the page. May be called manually for the file picker
2284 * dialog, because it closes by calling extension API functions that do not
2287 * TODO(hirono): This method is not called when Files.app is opend as a dialog
2288 * and is closed by the close button in the dialog frame. crbug.com/309967
2291 FileManager.prototype.onUnload_ = function() {
2292 if (this.directoryModel_)
2293 this.directoryModel_.dispose();
2294 if (this.volumeManager_)
2295 this.volumeManager_.dispose();
2296 if (this.filePopup_ &&
2297 this.filePopup_.contentWindow &&
2298 this.filePopup_.contentWindow.unload)
2299 this.filePopup_.contentWindow.unload(true /* exiting */);
2300 if (this.butterBar_)
2301 this.butterBar_.dispose();
2302 if (this.fileOperationManager_) {
2303 if (this.onCopyProgressBound_) {
2304 this.fileOperationManager_.removeEventListener(
2305 'copy-progress', this.onCopyProgressBound_);
2307 if (this.onEntryChangedBound_) {
2308 this.fileOperationManager_.removeEventListener(
2309 'entry-changed', this.onEntryChangedBound_);
2312 window.closing = true;
2313 if (this.backgroundPage_ && util.platform.runningInBrowser())
2314 this.backgroundPage_.maybeCloseBackgroundPage();
2317 FileManager.prototype.initiateRename = function() {
2318 var item = this.currentList_.ensureLeadItemExists();
2321 var label = item.querySelector('.filename-label');
2322 var input = this.renameInput_;
2324 input.value = label.textContent;
2325 label.parentNode.setAttribute('renaming', '');
2326 label.parentNode.appendChild(input);
2328 var selectionEnd = input.value.lastIndexOf('.');
2329 if (selectionEnd == -1) {
2332 input.selectionStart = 0;
2333 input.selectionEnd = selectionEnd;
2336 // This has to be set late in the process so we don't handle spurious
2338 input.currentEntry = this.currentList_.dataModel.item(item.listIndex);
2342 * @type {Event} Key event.
2345 FileManager.prototype.onRenameInputKeyDown_ = function(event) {
2346 if (!this.isRenamingInProgress())
2349 // Do not move selection or lead item in list during rename.
2350 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
2351 event.stopPropagation();
2354 switch (util.getKeyModifiers(event) + event.keyCode) {
2355 case '27': // Escape
2356 this.cancelRename_();
2357 event.preventDefault();
2361 this.commitRename_();
2362 event.preventDefault();
2368 * @type {Event} Blur event.
2371 FileManager.prototype.onRenameInputBlur_ = function(event) {
2372 if (this.isRenamingInProgress() && !this.renameInput_.validation_)
2373 this.commitRename_();
2379 FileManager.prototype.commitRename_ = function() {
2380 var input = this.renameInput_;
2381 var entry = input.currentEntry;
2382 var newName = input.value;
2384 if (newName == entry.name) {
2385 this.cancelRename_();
2389 var nameNode = this.findListItemForNode_(this.renameInput_).
2390 querySelector('.filename-label');
2392 input.validation_ = true;
2393 var validationDone = function(valid) {
2394 input.validation_ = false;
2395 // Alert dialog restores focus unless the item removed from DOM.
2396 if (this.document_.activeElement != input)
2397 this.cancelRename_();
2401 // Validation succeeded. Do renaming.
2403 this.cancelRename_();
2404 // Optimistically apply new name immediately to avoid flickering in
2406 nameNode.textContent = newName;
2410 function(newEntry) {
2411 this.directoryModel_.onRenameEntry(entry, newEntry);
2414 // Write back to the old name.
2415 nameNode.textContent = entry.name;
2417 // Show error dialog.
2419 if (error.code == FileError.PATH_EXISTS_ERR ||
2420 error.code == FileError.TYPE_MISMATCH_ERR) {
2421 // Check the existing entry is file or not.
2422 // 1) If the entry is a file:
2423 // a) If we get PATH_EXISTS_ERR, a file exists.
2424 // b) If we get TYPE_MISMATCH_ERR, a directory exists.
2425 // 2) If the entry is a directory:
2426 // a) If we get PATH_EXISTS_ERR, a directory exists.
2427 // b) If we get TYPE_MISMATCH_ERR, a file exists.
2429 (entry.isFile && error.code == FileError.PATH_EXISTS_ERR) ||
2430 (!entry.isFile && error.code == FileError.TYPE_MISMATCH_ERR) ?
2431 'FILE_ALREADY_EXISTS' :
2432 'DIRECTORY_ALREADY_EXISTS',
2435 message = strf('ERROR_RENAMING', entry.name,
2436 util.getFileErrorString(err.code));
2439 this.alert.show(message);
2443 // TODO(haruki): this.getCurrentDirectoryURL() might not return the actual
2444 // parent if the directory content is a search result. Fix it to do proper
2446 this.validateFileName_(this.getCurrentDirectoryURL(),
2448 validationDone.bind(this));
2454 FileManager.prototype.cancelRename_ = function() {
2455 this.renameInput_.currentEntry = null;
2457 var parent = this.renameInput_.parentNode;
2459 parent.removeAttribute('renaming');
2460 parent.removeChild(this.renameInput_);
2465 * @param {Event} Key event.
2468 FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
2469 var enabled = this.selectionHandler_.updateOkButton();
2471 (util.getKeyModifiers(event) + event.keyCode) == '13' /* Enter */)
2476 * @param {Event} Focus event.
2479 FileManager.prototype.onFilenameInputFocus_ = function(event) {
2480 var input = this.filenameInput_;
2482 // On focus we want to select everything but the extension, but
2483 // Chrome will select-all after the focus event completes. We
2484 // schedule a timeout to alter the focus after that happens.
2485 setTimeout(function() {
2486 var selectionEnd = input.value.lastIndexOf('.');
2487 if (selectionEnd == -1) {
2490 input.selectionStart = 0;
2491 input.selectionEnd = selectionEnd;
2499 FileManager.prototype.onScanStarted_ = function() {
2500 if (this.scanInProgress_) {
2501 this.table_.list.endBatchUpdates();
2502 this.grid_.endBatchUpdates();
2505 if (this.commandHandler)
2506 this.commandHandler.updateAvailability();
2507 this.table_.list.startBatchUpdates();
2508 this.grid_.startBatchUpdates();
2509 this.scanInProgress_ = true;
2511 this.scanUpdatedAtLeastOnceOrCompleted_ = false;
2512 if (this.scanCompletedTimer_) {
2513 clearTimeout(this.scanCompletedTimer_);
2514 this.scanCompletedTimer_ = null;
2517 if (this.scanUpdatedTimer_) {
2518 clearTimeout(this.scanUpdatedTimer_);
2519 this.scanUpdatedTimer_ = null;
2522 if (this.spinner_.hidden) {
2523 this.cancelSpinnerTimeout_();
2524 this.showSpinnerTimeout_ =
2525 setTimeout(this.showSpinner_.bind(this, true), 500);
2532 FileManager.prototype.onScanCompleted_ = function() {
2533 if (!this.scanInProgress_) {
2534 console.error('Scan-completed event recieved. But scan is not started.');
2538 if (this.commandHandler)
2539 this.commandHandler.updateAvailability();
2540 this.hideSpinnerLater_();
2541 this.refreshCurrentDirectoryMetadata_();
2543 if (this.scanUpdatedTimer_) {
2544 clearTimeout(this.scanUpdatedTimer_);
2545 this.scanUpdatedTimer_ = null;
2548 // To avoid flickering postpone updating the ui by a small amount of time.
2549 // There is a high chance, that metadata will be received within 50 ms.
2550 this.scanCompletedTimer_ = setTimeout(function() {
2551 // Check if batch updates are already finished by onScanUpdated_().
2552 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2553 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2554 this.updateMiddleBarVisibility_();
2557 this.scanInProgress_ = false;
2558 this.table_.list.endBatchUpdates();
2559 this.grid_.endBatchUpdates();
2560 this.scanCompletedTimer_ = null;
2567 FileManager.prototype.onScanUpdated_ = function() {
2568 if (!this.scanInProgress_) {
2569 console.error('Scan-updated event recieved. But scan is not started.');
2573 if (this.scanUpdatedTimer_ || this.scanCompletedTimer_)
2576 // Show contents incrementally by finishing batch updated, but only after
2577 // 200ms elapsed, to avoid flickering when it is not necessary.
2578 this.scanUpdatedTimer_ = setTimeout(function() {
2579 // We need to hide the spinner only once.
2580 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2581 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2582 this.hideSpinnerLater_();
2583 this.updateMiddleBarVisibility_();
2587 if (this.scanInProgress_) {
2588 this.table_.list.endBatchUpdates();
2589 this.grid_.endBatchUpdates();
2590 this.table_.list.startBatchUpdates();
2591 this.grid_.startBatchUpdates();
2593 this.scanUpdatedTimer_ = null;
2600 FileManager.prototype.onScanCancelled_ = function() {
2601 if (!this.scanInProgress_) {
2602 console.error('Scan-cancelled event recieved. But scan is not started.');
2606 if (this.commandHandler)
2607 this.commandHandler.updateAvailability();
2608 this.hideSpinnerLater_();
2609 if (this.scanCompletedTimer_) {
2610 clearTimeout(this.scanCompletedTimer_);
2611 this.scanCompletedTimer_ = null;
2613 if (this.scanUpdatedTimer_) {
2614 clearTimeout(this.scanUpdatedTimer_);
2615 this.scanUpdatedTimer_ = null;
2617 // Finish unfinished batch updates.
2618 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2619 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2620 this.updateMiddleBarVisibility_();
2623 this.scanInProgress_ = false;
2624 this.table_.list.endBatchUpdates();
2625 this.grid_.endBatchUpdates();
2629 * Handle the 'rescan-completed' from the DirectoryModel.
2632 FileManager.prototype.onRescanCompleted_ = function() {
2633 this.refreshCurrentDirectoryMetadata_();
2634 this.selectionHandler_.onFileSelectionChanged();
2640 FileManager.prototype.cancelSpinnerTimeout_ = function() {
2641 if (this.showSpinnerTimeout_) {
2642 clearTimeout(this.showSpinnerTimeout_);
2643 this.showSpinnerTimeout_ = null;
2650 FileManager.prototype.hideSpinnerLater_ = function() {
2651 this.cancelSpinnerTimeout_();
2652 this.showSpinner_(false);
2656 * @param {boolean} on True to show, false to hide.
2659 FileManager.prototype.showSpinner_ = function(on) {
2660 if (on && this.directoryModel_ && this.directoryModel_.isScanning())
2661 this.spinner_.hidden = false;
2663 if (!on && (!this.directoryModel_ ||
2664 !this.directoryModel_.isScanning() ||
2665 this.directoryModel_.getFileList().length != 0)) {
2666 this.spinner_.hidden = true;
2670 FileManager.prototype.createNewFolder = function() {
2671 var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
2673 // Find a name that doesn't exist in the data model.
2674 var files = this.directoryModel_.getFileList();
2676 for (var i = 0; i < files.length; i++) {
2677 var name = files.item(i).name;
2678 // Filtering names prevents from conflicts with prototype's names
2680 if (name.substring(0, defaultName.length) == defaultName)
2684 var baseName = defaultName;
2689 var advance = function() {
2695 var current = function() {
2696 return baseName + separator + index + suffix;
2699 // Accessing hasOwnProperty is safe since hash properties filtered.
2700 while (hash.hasOwnProperty(current())) {
2705 var list = self.currentList_;
2706 var tryCreate = function() {
2707 self.directoryModel_.createDirectory(current(),
2708 onSuccess, onError);
2711 var onSuccess = function(entry) {
2712 metrics.recordUserAction('CreateNewFolder');
2713 list.selectedItem = entry;
2714 self.initiateRename();
2717 var onError = function(error) {
2718 self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
2719 util.getFileErrorString(error.code)));
2726 * @param {Event} event Click event.
2729 FileManager.prototype.onDetailViewButtonClick_ = function(event) {
2730 this.setListType(FileManager.ListType.DETAIL);
2731 this.currentList_.focus();
2735 * @param {Event} event Click event.
2738 FileManager.prototype.onThumbnailViewButtonClick_ = function(event) {
2739 this.setListType(FileManager.ListType.THUMBNAIL);
2740 this.currentList_.focus();
2744 * KeyDown event handler for the document.
2745 * @param {Event} event Key event.
2748 FileManager.prototype.onKeyDown_ = function(event) {
2749 if (event.srcElement === this.renameInput_) {
2750 // Ignore keydown handler in the rename input box.
2754 switch (util.getKeyModifiers(event) + event.keyCode) {
2755 case 'Ctrl-190': // Ctrl-. => Toggle filter files.
2756 this.fileFilter_.setFilterHidden(
2757 !this.fileFilter_.isFilterHiddenOn());
2758 event.preventDefault();
2761 case '27': // Escape => Cancel dialog.
2762 if (this.dialogType != DialogType.FULL_PAGE) {
2763 // If there is nothing else for ESC to do, then cancel the dialog.
2764 event.preventDefault();
2765 this.cancelButton_.click();
2772 * KeyDown event handler for the div#list-container element.
2773 * @param {Event} event Key event.
2776 FileManager.prototype.onListKeyDown_ = function(event) {
2777 if (event.srcElement.tagName == 'INPUT') {
2778 // Ignore keydown handler in the rename input box.
2782 switch (util.getKeyModifiers(event) + event.keyCode) {
2783 case '8': // Backspace => Up one directory.
2784 event.preventDefault();
2785 var path = this.getCurrentDirectory();
2786 if (path && !PathUtil.isRootPath(path)) {
2787 var path = path.replace(/\/[^\/]+$/, '');
2788 this.directoryModel_.changeDirectory(path);
2792 case '13': // Enter => Change directory or perform default action.
2793 // TODO(dgozman): move directory action to dispatchSelectionAction.
2794 var selection = this.getSelection();
2795 if (selection.totalCount == 1 &&
2796 selection.entries[0].isDirectory &&
2797 this.dialogType != DialogType.SELECT_FOLDER &&
2798 this.dialogType != DialogType.SELECT_UPLOAD_FOLDER) {
2799 event.preventDefault();
2800 this.onDirectoryAction_(selection.entries[0]);
2801 } else if (this.dispatchSelectionAction_()) {
2802 event.preventDefault();
2807 switch (event.keyIdentifier) {
2814 // When navigating with keyboard we hide the distracting mouse hover
2815 // highlighting until the user moves the mouse again.
2816 this.setNoHover_(true);
2822 * Suppress/restore hover highlighting in the list container.
2823 * @param {boolean} on True to temporarity hide hover state.
2826 FileManager.prototype.setNoHover_ = function(on) {
2828 this.listContainer_.classList.add('nohover');
2830 this.listContainer_.classList.remove('nohover');
2835 * KeyPress event handler for the div#list-container element.
2836 * @param {Event} event Key event.
2839 FileManager.prototype.onListKeyPress_ = function(event) {
2840 if (event.srcElement.tagName == 'INPUT') {
2841 // Ignore keypress handler in the rename input box.
2845 if (event.ctrlKey || event.metaKey || event.altKey)
2848 var now = new Date();
2849 var char = String.fromCharCode(event.charCode).toLowerCase();
2850 var text = now - this.textSearchState_.date > 1000 ? '' :
2851 this.textSearchState_.text;
2852 this.textSearchState_ = {text: text + char, date: now};
2854 this.doTextSearch_();
2858 * Mousemove event handler for the div#list-container element.
2859 * @param {Event} event Mouse event.
2862 FileManager.prototype.onListMouseMove_ = function(event) {
2863 // The user grabbed the mouse, restore the hover highlighting.
2864 this.setNoHover_(false);
2868 * Performs a 'text search' - selects a first list entry with name
2869 * starting with entered text (case-insensitive).
2872 FileManager.prototype.doTextSearch_ = function() {
2873 var text = this.textSearchState_.text;
2877 var dm = this.directoryModel_.getFileList();
2878 for (var index = 0; index < dm.length; ++index) {
2879 var name = dm.item(index).name;
2880 if (name.substring(0, text.length).toLowerCase() == text) {
2881 this.currentList_.selectionModel.selectedIndexes = [index];
2886 this.textSearchState_.text = '';
2890 * Handle a click of the cancel button. Closes the window.
2891 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
2893 * @param {Event} event The click event.
2896 FileManager.prototype.onCancel_ = function(event) {
2897 chrome.fileBrowserPrivate.cancelDialog();
2903 * Resolves selected file urls returned from an Open dialog.
2905 * For drive files this involves some special treatment.
2906 * Starts getting drive files if needed.
2908 * @param {Array.<string>} fileUrls Drive URLs.
2909 * @param {function(Array.<string>)} callback To be called with fixed URLs.
2912 FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) {
2913 if (this.isOnDrive()) {
2914 chrome.fileBrowserPrivate.getDriveFiles(
2916 function(localPaths) {
2925 * Closes this modal dialog with some files selected.
2926 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
2927 * @param {Object} selection Contains urls, filterIndex and multiple fields.
2930 FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) {
2932 function callback() {
2936 if (selection.multiple) {
2937 chrome.fileBrowserPrivate.selectFiles(
2938 selection.urls, this.params_.shouldReturnLocalPath, callback);
2940 var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE);
2941 chrome.fileBrowserPrivate.selectFile(
2942 selection.urls[0], selection.filterIndex, forOpening,
2943 this.params_.shouldReturnLocalPath, callback);
2948 * Tries to close this modal dialog with some files selected.
2949 * Performs preprocessing if needed (e.g. for Drive).
2950 * @param {Object} selection Contains urls, filterIndex and multiple fields.
2953 FileManager.prototype.selectFilesAndClose_ = function(selection) {
2954 if (!this.isOnDrive() ||
2955 this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
2956 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
2960 var shade = this.document_.createElement('div');
2961 shade.className = 'shade';
2962 var footer = this.dialogDom_.querySelector('.button-panel');
2963 var progress = footer.querySelector('.progress-track');
2964 progress.style.width = '0%';
2965 var cancelled = false;
2967 var progressMap = {};
2968 var filesStarted = 0;
2969 var filesTotal = selection.urls.length;
2970 for (var index = 0; index < selection.urls.length; index++) {
2971 progressMap[selection.urls[index]] = -1;
2973 var lastPercent = 0;
2977 var onFileTransfersUpdated = function(statusList) {
2978 for (var index = 0; index < statusList.length; index++) {
2979 var status = statusList[index];
2980 var escaped = encodeURI(status.fileUrl);
2981 if (!(escaped in progressMap)) continue;
2982 if (status.total == -1) continue;
2984 var old = progressMap[escaped];
2986 // -1 means we don't know file size yet.
2987 bytesTotal += status.total;
2991 bytesDone += status.processed - old;
2992 progressMap[escaped] = status.processed;
2995 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
2996 // For files we don't have information about, assume the progress is zero.
2997 percent = percent * filesStarted / filesTotal * 100;
2998 // Do not decrease the progress. This may happen, if first downloaded
2999 // file is small, and the second one is large.
3000 lastPercent = Math.max(lastPercent, percent);
3001 progress.style.width = lastPercent + '%';
3004 var setup = function() {
3005 this.document_.querySelector('.dialog-container').appendChild(shade);
3006 setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100);
3007 footer.setAttribute('progress', 'progress');
3008 this.cancelButton_.removeEventListener('click', this.onCancelBound_);
3009 this.cancelButton_.addEventListener('click', onCancel);
3010 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
3011 onFileTransfersUpdated);
3014 var cleanup = function() {
3015 shade.parentNode.removeChild(shade);
3016 footer.removeAttribute('progress');
3017 this.cancelButton_.removeEventListener('click', onCancel);
3018 this.cancelButton_.addEventListener('click', this.onCancelBound_);
3019 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
3020 onFileTransfersUpdated);
3023 var onCancel = function() {
3025 // According to API cancel may fail, but there is no proper UI to reflect
3026 // this. So, we just silently assume that everything is cancelled.
3027 chrome.fileBrowserPrivate.cancelFileTransfers(
3028 selection.urls, function(response) {});
3032 var onResolved = function(resolvedUrls) {
3033 if (cancelled) return;
3035 selection.urls = resolvedUrls;
3036 // Call next method on a timeout, as it's unsafe to
3037 // close a window from a callback.
3038 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3041 var onProperties = function(properties) {
3042 for (var i = 0; i < properties.length; i++) {
3043 if (!properties[i] || properties[i].present) {
3044 // For files already in GCache, we don't get any transfer updates.
3048 this.resolveSelectResults_(selection.urls, onResolved);
3052 this.metadataCache_.get(selection.urls, 'drive', onProperties);
3056 * Handle a click of the ok button.
3058 * The ok button has different UI labels depending on the type of dialog, but
3059 * in code it's always referred to as 'ok'.
3061 * @param {Event} event The click event.
3064 FileManager.prototype.onOk_ = function(event) {
3065 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3066 // Save-as doesn't require a valid selection from the list, since
3067 // we're going to take the filename from the text input.
3068 var filename = this.filenameInput_.value;
3070 throw new Error('Missing filename!');
3072 var directory = this.getCurrentDirectoryEntry();
3073 var currentDirUrl = directory.toURL();
3074 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
3075 currentDirUrl += '/';
3076 this.validateFileName_(currentDirUrl, filename, function(isValid) {
3080 if (util.isFakeDirectoryEntry(directory)) {
3081 // Can't save a file into a fake directory.
3085 var selectFileAndClose = function() {
3086 this.selectFilesAndClose_({
3087 urls: [currentDirUrl + encodeURIComponent(filename)],
3089 filterIndex: this.getSelectedFilterIndex_(filename)
3094 filename, {create: false},
3096 // An existing file is found. Show confirmation dialog to
3097 // overwrite it. If the user select "OK" on the dialog, save it.
3098 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
3099 selectFileAndClose);
3102 if (error.code == FileError.NOT_FOUND_ERR) {
3103 // The file does not exist, so it should be ok to create a
3105 selectFileAndClose();
3108 if (error.code == FileError.TYPE_MISMATCH_ERR) {
3109 // An directory is found.
3110 // Do not allow to overwrite directory.
3111 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
3115 // Unexpected error.
3116 console.error('File save failed: ' + error.code);
3123 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
3125 if ((this.dialogType == DialogType.SELECT_FOLDER ||
3126 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER) &&
3127 selectedIndexes.length == 0) {
3128 var url = this.getCurrentDirectoryURL();
3129 var singleSelection = {
3132 filterIndex: this.getSelectedFilterIndex_()
3134 this.selectFilesAndClose_(singleSelection);
3138 // All other dialog types require at least one selected list item.
3139 // The logic to control whether or not the ok button is enabled should
3140 // prevent us from ever getting here, but we sanity check to be sure.
3141 if (!selectedIndexes.length)
3142 throw new Error('Nothing selected!');
3144 var dm = this.directoryModel_.getFileList();
3145 for (var i = 0; i < selectedIndexes.length; i++) {
3146 var entry = dm.item(selectedIndexes[i]);
3148 console.error('Error locating selected file at index: ' + i);
3152 files.push(entry.toURL());
3155 // Multi-file selection has no other restrictions.
3156 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
3157 var multipleSelection = {
3161 this.selectFilesAndClose_(multipleSelection);
3165 // Everything else must have exactly one.
3166 if (files.length > 1)
3167 throw new Error('Too many files selected!');
3169 var selectedEntry = dm.item(selectedIndexes[0]);
3171 if (this.dialogType == DialogType.SELECT_FOLDER ||
3172 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER) {
3173 if (!selectedEntry.isDirectory)
3174 throw new Error('Selected entry is not a folder!');
3175 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
3176 if (!selectedEntry.isFile)
3177 throw new Error('Selected entry is not a file!');
3180 var singleSelection = {
3183 filterIndex: this.getSelectedFilterIndex_()
3185 this.selectFilesAndClose_(singleSelection);
3189 * Verifies the user entered name for file or folder to be created or
3190 * renamed to. Name restrictions must correspond to File API restrictions
3191 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is
3192 * out of date (spec is
3193 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to
3194 * be fixed. Shows message box if the name is invalid.
3196 * It also verifies if the name length is in the limit of the filesystem.
3198 * @param {string} parentUrl The URL of the parent directory entry.
3199 * @param {string} name New file or folder name.
3200 * @param {function} onDone Function to invoke when user closes the
3201 * warning box or immediatelly if file name is correct. If the name was
3202 * valid it is passed true, and false otherwise.
3205 FileManager.prototype.validateFileName_ = function(parentUrl, name, onDone) {
3207 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
3209 msg = strf('ERROR_INVALID_CHARACTER', testResult[0]);
3210 } else if (/^\s*$/i.test(name)) {
3211 msg = str('ERROR_WHITESPACE_NAME');
3212 } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) {
3213 msg = str('ERROR_RESERVED_NAME');
3214 } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') {
3215 msg = str('ERROR_HIDDEN_NAME');
3219 this.alert.show(msg, function() {
3226 chrome.fileBrowserPrivate.validatePathNameLength(
3227 parentUrl, name, function(valid) {
3229 self.alert.show(str('ERROR_LONG_NAME'),
3230 function() { onDone(false); });
3238 * Handler invoked on preference setting in drive context menu.
3240 * @param {string} pref The preference to alter.
3241 * @param {boolean} inverted Invert the value if true.
3242 * @param {Event} event The click event.
3245 FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) {
3246 var newValue = !event.target.hasAttribute('checked');
3248 event.target.setAttribute('checked', 'checked');
3250 event.target.removeAttribute('checked');
3252 var changeInfo = {};
3253 changeInfo[pref] = inverted ? !newValue : newValue;
3254 chrome.fileBrowserPrivate.setPreferences(changeInfo);
3258 * Invoked when the search box is changed.
3260 * @param {Event} event The changed event.
3263 FileManager.prototype.onSearchBoxUpdate_ = function(event) {
3264 var searchString = this.searchBox_.value;
3266 if (this.isOnDrive()) {
3267 // When the search text is changed, finishes the search and showes back
3268 // the last directory by passing an empty string to
3269 // {@code DirectoryModel.search()}.
3270 if (this.directoryModel_.isSearching() &&
3271 this.lastSearchQuery_ != searchString) {
3275 // On drive, incremental search is not invoked since we have an auto-
3276 // complete suggestion instead.
3280 this.search_(searchString);
3284 * Handle the search clear button click.
3287 FileManager.prototype.onSearchClearButtonClick_ = function() {
3288 this.ui_.searchBox.clear();
3289 this.onSearchBoxUpdate_();
3293 * Search files and update the list with the search result.
3295 * @param {string} searchString String to be searched with.
3298 FileManager.prototype.search_ = function(searchString) {
3299 var noResultsDiv = this.document_.getElementById('no-search-results');
3301 var reportEmptySearchResults = function() {
3302 if (this.directoryModel_.getFileList().length === 0) {
3303 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
3304 // hence we escapes |searchString| here.
3305 var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
3306 util.htmlEscape(searchString));
3307 noResultsDiv.innerHTML = html;
3308 noResultsDiv.setAttribute('show', 'true');
3310 noResultsDiv.removeAttribute('show');
3314 var hideNoResultsDiv = function() {
3315 noResultsDiv.removeAttribute('show');
3318 this.doSearch(searchString,
3319 reportEmptySearchResults.bind(this),
3320 hideNoResultsDiv.bind(this));
3324 * Performs search and displays results.
3326 * @param {string} query Query that will be searched for.
3327 * @param {function()=} opt_onSearchRescan Function that will be called when
3328 * the search directory is rescanned (i.e. search results are displayed).
3329 * @param {function()=} opt_onClearSearch Function to be called when search
3330 * state gets cleared.
3332 FileManager.prototype.doSearch = function(
3333 searchString, opt_onSearchRescan, opt_onClearSearch) {
3334 var onSearchRescan = opt_onSearchRescan || function() {};
3335 var onClearSearch = opt_onClearSearch || function() {};
3337 this.lastSearchQuery_ = searchString;
3338 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
3342 * Requests autocomplete suggestions for files on Drive.
3343 * Once the suggestions are returned, the autocomplete popup will show up.
3345 * @param {string} query The text to autocomplete from.
3348 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
3349 query = query.trimLeft();
3351 // Only Drive supports auto-compelete
3352 if (!this.isOnDrive())
3355 // Remember the most recent query. If there is an other request in progress,
3356 // then it's result will be discarded and it will call a new request for
3358 this.lastAutocompleteQuery_ = query;
3359 if (this.autocompleteSuggestionsBusy_)
3362 // The autocomplete list should be resized and repositioned here as the
3363 // search box is resized when it's focused.
3364 this.autocompleteList_.syncWidthAndPositionToInput();
3367 this.autocompleteList_.suggestions = [];
3371 var headerItem = {isHeaderItem: true, searchQuery: query};
3372 if (!this.autocompleteList_.dataModel ||
3373 this.autocompleteList_.dataModel.length == 0)
3374 this.autocompleteList_.suggestions = [headerItem];
3376 // Updates only the head item to prevent a flickering on typing.
3377 this.autocompleteList_.dataModel.splice(0, 1, headerItem);
3379 this.autocompleteSuggestionsBusy_ = true;
3381 var searchParams = {
3386 chrome.fileBrowserPrivate.searchDriveMetadata(
3388 function(suggestions) {
3389 this.autocompleteSuggestionsBusy_ = false;
3391 // Discard results for previous requests and fire a new search
3392 // for the most recent query.
3393 if (query != this.lastAutocompleteQuery_) {
3394 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
3398 // Keeps the items in the suggestion list.
3399 this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
3404 * Opens the currently selected suggestion item.
3407 FileManager.prototype.openAutocompleteSuggestion_ = function() {
3408 var selectedItem = this.autocompleteList_.selectedItem;
3410 // If the entry is the search item or no entry is selected, just change to
3411 // the search result.
3412 if (!selectedItem || selectedItem.isHeaderItem) {
3413 var query = selectedItem ?
3414 selectedItem.searchQuery : this.searchBox_.value;
3415 this.search_(query);
3419 var entry = selectedItem.entry;
3420 // If the entry is a directory, just change the directory.
3421 if (entry.isDirectory) {
3422 this.onDirectoryAction_(entry);
3426 var urls = [entry.toURL()];
3429 // To open a file, first get the mime type.
3430 this.metadataCache_.get(urls, 'drive', function(props) {
3431 var mimeType = props[0].contentMimeType || '';
3432 var mimeTypes = [mimeType];
3433 var openIt = function() {
3434 if (self.dialogType == DialogType.FULL_PAGE) {
3435 var tasks = new FileTasks(self);
3436 tasks.init(urls, mimeTypes);
3437 tasks.executeDefault();
3443 // Change the current directory to the directory that contains the
3444 // selected file. Note that this is necessary for an image or a video,
3445 // which should be opened in the gallery mode, as the gallery mode
3446 // requires the entry to be in the current directory model. For
3447 // consistency, the current directory is always changed regardless of
3449 entry.getParent(function(parent) {
3450 var onDirectoryChanged = function(event) {
3451 self.directoryModel_.removeEventListener('scan-completed',
3452 onDirectoryChanged);
3453 self.directoryModel_.selectEntry(entry.name);
3456 // changeDirectory() returns immediately. We should wait until the
3457 // directory scan is complete.
3458 self.directoryModel_.addEventListener('scan-completed',
3459 onDirectoryChanged);
3460 self.directoryModel_.changeDirectory(
3463 // Remove the listner if the change directory failed.
3464 self.directoryModel_.removeEventListener('scan-completed',
3465 onDirectoryChanged);
3472 * Opens the default app change dialog.
3474 FileManager.prototype.showChangeDefaultAppPicker = function() {
3475 var onActionsReady = function(actions, rememberedActionId) {
3477 var defaultIndex = -1;
3478 for (var i = 0; i < actions.length; i++) {
3479 if (actions[i].hidden)
3481 var title = actions[i].title;
3482 if (actions[i].id == rememberedActionId) {
3483 title += ' ' + loadTimeData.getString('DEFAULT_ACTION_LABEL');
3489 class: actions[i].class,
3490 iconUrl: actions[i].icon100
3494 var show = this.defaultTaskPicker.showOkCancelDialog(
3495 str('CHANGE_DEFAULT_APP_BUTTON_LABEL'),
3500 ActionChoiceUtil.setRememberedActionId(action.id);
3503 console.error('DefaultTaskPicker can\'t be shown.');
3506 ActionChoiceUtil.getDefinedActions(loadTimeData, function(actions) {
3507 ActionChoiceUtil.getRememberedActionId(function(actionId) {
3508 onActionsReady(actions, actionId);
3513 FileManager.prototype.decorateSplitter = function(splitterElement) {
3516 var Splitter = cr.ui.Splitter;
3518 var customSplitter = cr.ui.define('div');
3520 customSplitter.prototype = {
3521 __proto__: Splitter.prototype,
3523 handleSplitterDragStart: function(e) {
3524 Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
3525 this.ownerDocument.documentElement.classList.add('col-resize');
3528 handleSplitterDragMove: function(deltaX) {
3529 Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
3533 handleSplitterDragEnd: function(e) {
3534 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
3535 this.ownerDocument.documentElement.classList.remove('col-resize');
3539 customSplitter.decorate(splitterElement);
3543 * Updates default action menu item to match passed taskItem (icon,
3544 * label and action).
3546 * @param {Object} defaultItem - taskItem to match.
3547 * @param {boolean} isMultiple - if multiple tasks available.
3549 FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
3552 if (defaultItem.iconType) {
3553 this.defaultActionMenuItem_.style.backgroundImage = '';
3554 this.defaultActionMenuItem_.setAttribute('file-type-icon',
3555 defaultItem.iconType);
3556 } else if (defaultItem.iconUrl) {
3557 this.defaultActionMenuItem_.style.backgroundImage =
3558 'url(' + defaultItem.iconUrl + ')';
3560 this.defaultActionMenuItem_.style.backgroundImage = '';
3563 this.defaultActionMenuItem_.label = defaultItem.title;
3564 this.defaultActionMenuItem_.disabled = !!defaultItem.disabled;
3565 this.defaultActionMenuItem_.taskId = defaultItem.taskId;
3568 var defaultActionSeparator =
3569 this.dialogDom_.querySelector('#default-action-separator');
3571 this.openWithCommand_.canExecuteChange();
3572 this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
3573 this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled;
3575 this.defaultActionMenuItem_.hidden = !defaultItem;
3576 defaultActionSeparator.hidden = !defaultItem;
3580 * Window beforeunload handler.
3581 * @return {string} Message to show. Ignored when running as a packaged app.
3584 FileManager.prototype.onBeforeUnload_ = function() {
3585 if (this.filePopup_ &&
3586 this.filePopup_.contentWindow &&
3587 this.filePopup_.contentWindow.beforeunload) {
3588 // The gallery might want to prevent the unload if it is busy.
3589 return this.filePopup_.contentWindow.beforeunload();
3595 * @return {FileSelection} Selection object.
3597 FileManager.prototype.getSelection = function() {
3598 return this.selectionHandler_.selection;
3602 * @return {ArrayDataModel} File list.
3604 FileManager.prototype.getFileList = function() {
3605 return this.directoryModel_.getFileList();
3609 * @return {cr.ui.List} Current list object.
3611 FileManager.prototype.getCurrentList = function() {
3612 return this.currentList_;
3616 * Retrieve the preferences of the files.app. This method caches the result
3617 * and returns it unless opt_update is true.
3618 * @param {function(Object.<string, *>)} callback Callback to get the
3620 * @param {boolean=} opt_update If is's true, don't use the cache and
3621 * retrieve latest preference. Default is false.
3624 FileManager.prototype.getPreferences_ = function(callback, opt_update) {
3625 if (!opt_update && this.preferences_ !== undefined) {
3626 callback(this.preferences_);
3630 chrome.fileBrowserPrivate.getPreferences(function(prefs) {
3631 this.preferences_ = prefs;