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;
42 * VolumeInfo of the current volume.
46 this.currentVolumeInfo_ = null;
49 FileManager.prototype = {
50 __proto__: cr.EventTarget.prototype,
51 get directoryModel() {
52 return this.directoryModel_;
54 get navigationList() {
55 return this.navigationList_;
58 return this.document_;
60 get fileTransferController() {
61 return this.fileTransferController_;
63 get backgroundPage() {
64 return this.backgroundPage_;
67 return this.volumeManager_;
75 * Unload the file manager.
76 * Used by background.js (when running in the packaged mode).
79 fileManager.onBeforeUnload_();
80 fileManager.onUnload_();
84 * List of dialog types.
86 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
87 * FULL_PAGE which is specific to this code.
92 SELECT_FOLDER: 'folder',
93 SELECT_UPLOAD_FOLDER: 'upload-folder',
94 SELECT_SAVEAS_FILE: 'saveas-file',
95 SELECT_OPEN_FILE: 'open-file',
96 SELECT_OPEN_MULTI_FILE: 'open-multi-file',
97 FULL_PAGE: 'full-page'
101 * @param {string} type Dialog type.
102 * @return {boolean} Whether the type is modal.
104 DialogType.isModal = function(type) {
105 return type == DialogType.SELECT_FOLDER ||
106 type == DialogType.SELECT_UPLOAD_FOLDER ||
107 type == DialogType.SELECT_SAVEAS_FILE ||
108 type == DialogType.SELECT_OPEN_FILE ||
109 type == DialogType.SELECT_OPEN_MULTI_FILE;
113 * @param {string} type Dialog type.
114 * @return {boolean} Whether the type is open dialog.
116 DialogType.isOpenDialog = function(type) {
117 return type == DialogType.SELECT_OPEN_FILE ||
118 type == DialogType.SELECT_OPEN_MULTI_FILE;
122 * @param {string} type Dialog type.
123 * @return {boolean} Whether the type is folder selection dialog.
125 DialogType.isFolderDialog = function(type) {
126 return type == DialogType.SELECT_FOLDER ||
127 type == DialogType.SELECT_UPLOAD_FOLDER;
131 * Bottom margin of the list and tree for transparent preview panel.
134 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
136 // Anonymous "namespace".
139 // Private variables and helper functions.
142 * Number of milliseconds in a day.
144 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
147 * Some UI elements react on a single click and standard double click handling
148 * leads to confusing results. We ignore a second click if it comes soon
151 var DOUBLE_CLICK_TIMEOUT = 200;
154 * Update the element to display the information about remaining space for
156 * @param {!Element} spaceInnerBar Block element for a percentage bar
157 * representing the remaining space.
158 * @param {!Element} spaceInfoLabel Inline element to contain the message.
159 * @param {!Element} spaceOuterBar Block element around the percentage bar.
161 var updateSpaceInfo = function(
162 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
163 spaceInnerBar.removeAttribute('pending');
164 if (sizeStatsResult) {
165 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
166 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
169 sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
170 spaceInnerBar.style.width =
171 (100 * usedSpace / sizeStatsResult.totalSize) + '%';
173 spaceOuterBar.hidden = false;
175 spaceOuterBar.hidden = true;
176 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
182 FileManager.ListType = {
187 FileManager.prototype.initPreferences_ = function(callback) {
188 var group = new AsyncUtil.Group();
190 // DRIVE preferences should be initialized before creating DirectoryModel
191 // to rebuild the roots list.
192 group.add(this.getPreferences_.bind(this));
194 // Get startup preferences.
195 this.viewOptions_ = {};
196 group.add(function(done) {
197 util.platform.getPreference(this.startupPrefName_, function(value) {
198 // Load the global default options.
200 this.viewOptions_ = JSON.parse(value);
202 // Override with window-specific options.
203 if (window.appState && window.appState.viewOptions) {
204 for (var key in window.appState.viewOptions) {
205 if (window.appState.viewOptions.hasOwnProperty(key))
206 this.viewOptions_[key] = window.appState.viewOptions[key];
213 // Get the command line option.
214 group.add(function(done) {
215 chrome.commandLinePrivate.hasSwitch(
216 'file-manager-show-checkboxes', function(flag) {
217 this.showCheckboxes_ = flag;
226 * One time initialization for the file system and related things.
228 * @param {function()} callback Completion callback.
231 FileManager.prototype.initFileSystemUI_ = function(callback) {
232 this.table_.startBatchUpdates();
233 this.grid_.startBatchUpdates();
235 this.initFileList_();
236 this.setupCurrentDirectory_();
238 // PyAuto tests monitor this state by polling this variable
239 this.__defineGetter__('workerInitialized_', function() {
240 return this.metadataCache_.isInitialized();
243 this.initDateTimeFormatters_();
247 // Get the 'allowRedeemOffers' preference before launching
248 // FileListBannerController.
249 this.getPreferences_(function(pref) {
250 /** @type {boolean} */
251 var showOffers = pref['allowRedeemOffers'];
252 self.bannersController_ = new FileListBannerController(
253 self.directoryModel_, self.volumeManager_, self.document_,
255 self.bannersController_.addEventListener('relayout',
256 self.onResize_.bind(self));
259 var dm = this.directoryModel_;
260 dm.addEventListener('directory-changed',
261 this.onDirectoryChanged_.bind(this));
262 dm.addEventListener('begin-update-files', function() {
263 self.currentList_.startBatchUpdates();
265 dm.addEventListener('end-update-files', function() {
266 self.restoreItemBeingRenamed_();
267 self.currentList_.endBatchUpdates();
269 dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
270 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
271 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
272 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
273 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
274 dm.addEventListener('rescan-completed',
275 this.onRescanCompleted_.bind(this));
277 this.directoryTree_.addEventListener('change', function() {
278 this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
281 var stateChangeHandler =
282 this.onPreferencesChanged_.bind(this);
283 chrome.fileBrowserPrivate.onPreferencesChanged.addListener(
285 stateChangeHandler();
287 var driveConnectionChangedHandler =
288 this.onDriveConnectionChanged_.bind(this);
289 this.volumeManager_.addEventListener('drive-connection-changed',
290 driveConnectionChangedHandler);
291 driveConnectionChangedHandler();
293 // Set the initial focus.
295 // Set it as a fallback when there is no focus.
296 this.document_.addEventListener('focusout', function(e) {
297 setTimeout(function() {
298 // When there is no focus, the active element is the <body>.
299 if (this.document_.activeElement == this.document_.body)
304 this.initDataTransferOperations_();
306 this.initContextMenus_();
307 this.initCommands_();
309 this.updateFileTypeFilter_();
311 this.selectionHandler_.onFileSelectionChanged();
313 this.table_.endBatchUpdates();
314 this.grid_.endBatchUpdates();
320 * If |item| in the directory tree is behind the preview panel, scrolls up the
321 * parent view and make the item visible. This should be called when:
322 * - the selected item is changed in the directory tree.
323 * - the visibility of the the preview panel is changed.
327 FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ =
329 var selectedSubTree = this.directoryTree_.selectedItem;
330 if (!selectedSubTree)
332 var item = selectedSubTree.rowElement;
333 var parentView = this.directoryTree_;
335 var itemRect = item.getBoundingClientRect();
339 var listRect = parentView.getBoundingClientRect();
343 var previewPanel = this.dialogDom_.querySelector('.preview-panel');
344 var previewPanelRect = previewPanel.getBoundingClientRect();
345 var panelHeight = previewPanelRect ? previewPanelRect.height : 0;
347 var itemBottom = itemRect.bottom;
348 var listBottom = listRect.bottom - panelHeight;
350 if (itemBottom > listBottom) {
351 var scrollOffset = itemBottom - listBottom;
352 parentView.scrollTop += scrollOffset;
359 FileManager.prototype.initDateTimeFormatters_ = function() {
360 var use12hourClock = !this.preferences_['use24hourClock'];
361 this.table_.setDateTimeFormat(use12hourClock);
367 FileManager.prototype.initDataTransferOperations_ = function() {
368 this.fileOperationManager_ =
369 this.backgroundPage_.background.fileOperationManager;
371 // CopyManager are required for 'Delete' operation in
372 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
373 if (this.dialogType != DialogType.FULL_PAGE) return;
375 // TODO(hidehiko): Extract FileOperationManager related code from
376 // FileManager to simplify it.
377 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
378 this.fileOperationManager_.addEventListener(
379 'copy-progress', this.onCopyProgressBound_);
381 this.onEntryChangedBound_ = this.onEntryChanged_.bind(this);
382 this.fileOperationManager_.addEventListener(
383 'entry-changed', this.onEntryChangedBound_);
385 var controller = this.fileTransferController_ =
386 new FileTransferController(this.document_,
387 this.fileOperationManager_,
389 this.directoryModel_,
390 this.volumeManager_);
391 controller.attachDragSource(this.table_.list);
392 controller.attachFileListDropTarget(this.table_.list);
393 controller.attachDragSource(this.grid_);
394 controller.attachFileListDropTarget(this.grid_);
395 controller.attachTreeDropTarget(this.directoryTree_);
396 controller.attachNavigationListDropTarget(this.navigationList_, true);
397 controller.attachCopyPasteHandlers();
398 controller.addEventListener('selection-copied',
399 this.blinkSelection.bind(this));
400 controller.addEventListener('selection-cut',
401 this.blinkSelection.bind(this));
402 controller.addEventListener('source-not-found',
403 this.onSourceNotFound_.bind(this));
407 * Handles an error that the source entry of file operation is not found.
410 FileManager.prototype.onSourceNotFound_ = function(event) {
411 // Ensure this.sourceNotFoundErrorCount_ is integer.
412 this.sourceNotFoundErrorCount_ = ~~this.sourceNotFoundErrorCount_;
413 var item = new ProgressCenterItem();
414 item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_;
415 if (event.progressType === ProgressItemType.COPY)
416 item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', event.fileName);
417 else if (event.progressType === ProgressItemType.MOVE)
418 item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', event.fileName);
419 item.state = ProgressItemState.ERROR;
420 this.backgroundPage_.background.progressCenter.updateItem(item);
421 this.sourceNotFoundErrorCount_++;
425 * One-time initialization of context menus.
428 FileManager.prototype.initContextMenus_ = function() {
429 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
430 cr.ui.Menu.decorate(this.fileContextMenu_);
432 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
433 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
434 this.fileContextMenu_);
435 cr.ui.contextMenuHandler.setContextMenu(
436 this.document_.querySelector('.drive-welcome.page'),
437 this.fileContextMenu_);
439 this.rootsContextMenu_ =
440 this.dialogDom_.querySelector('#roots-context-menu');
441 cr.ui.Menu.decorate(this.rootsContextMenu_);
442 this.navigationList_.setContextMenu(this.rootsContextMenu_);
444 this.directoryTreeContextMenu_ =
445 this.dialogDom_.querySelector('#directory-tree-context-menu');
446 cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
447 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
449 this.textContextMenu_ =
450 this.dialogDom_.querySelector('#text-context-menu');
451 cr.ui.Menu.decorate(this.textContextMenu_);
453 this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
454 this.gearButton_.addEventListener('menushow',
455 this.refreshRemainingSpace_.bind(this,
456 false /* Without loading caption. */));
457 chrome.fileBrowserPrivate.onDesktopChanged.addListener(function() {
458 this.updateVisitDesktopMenus_();
459 this.ui_.updateProfileBadge();
461 chrome.fileBrowserPrivate.onProfileAdded.addListener(
462 this.updateVisitDesktopMenus_.bind(this));
463 this.updateVisitDesktopMenus_();
465 this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
467 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
469 if (this.dialogType == DialogType.FULL_PAGE) {
470 // This is to prevent the buttons from stealing focus on mouse down.
471 var preventFocus = function(event) {
472 event.preventDefault();
475 var maximizeButton = this.dialogDom_.querySelector('#maximize-button');
476 maximizeButton.addEventListener('click', this.onMaximize.bind(this));
477 maximizeButton.addEventListener('mousedown', preventFocus);
479 var closeButton = this.dialogDom_.querySelector('#close-button');
480 closeButton.addEventListener('click', this.onClose.bind(this));
481 closeButton.addEventListener('mousedown', preventFocus);
484 this.syncButton.checkable = true;
485 this.hostedButton.checkable = true;
486 this.detailViewButton_.checkable = true;
487 this.thumbnailViewButton_.checkable = true;
489 if (util.platform.runningInBrowser()) {
490 // Suppresses the default context menu.
491 this.dialogDom_.addEventListener('contextmenu', function(e) {
498 FileManager.prototype.onMaximize = function() {
499 var appWindow = chrome.app.window.current();
500 if (appWindow.isMaximized())
503 appWindow.maximize();
506 FileManager.prototype.onClose = function() {
511 * One-time initialization of commands.
514 FileManager.prototype.initCommands_ = function() {
515 this.commandHandler = new CommandHandler(this);
517 // TODO(hirono): Move the following block to the UI part.
518 var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
519 for (var j = 0; j < commandButtons.length; j++)
520 CommandButton.decorate(commandButtons[j]);
522 var inputs = this.dialogDom_.querySelectorAll(
523 'input[type=text], input[type=search], textarea');
524 for (var i = 0; i < inputs.length; i++) {
525 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
526 this.registerInputCommands_(inputs[i]);
529 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
530 this.textContextMenu_);
531 this.registerInputCommands_(this.renameInput_);
532 this.document_.addEventListener('command',
533 this.setNoHover_.bind(this, true));
537 * Registers cut, copy, paste and delete commands on input element.
539 * @param {Node} node Text input element to register on.
542 FileManager.prototype.registerInputCommands_ = function(node) {
543 CommandUtil.forceDefaultHandler(node, 'cut');
544 CommandUtil.forceDefaultHandler(node, 'copy');
545 CommandUtil.forceDefaultHandler(node, 'paste');
546 CommandUtil.forceDefaultHandler(node, 'delete');
547 node.addEventListener('keydown', function(e) {
548 var key = util.getKeyModifiers(e) + e.keyCode;
549 if (key === '190' /* '/' */ || key === '191' /* '.' */) {
550 // If this key event is propagated, this is handled search command,
551 // which calls 'preventDefault' method.
558 * Entry point of the initialization.
559 * This method is called from main.js.
561 FileManager.prototype.initializeCore = function() {
562 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
563 this.initializeQueue_.add(this.initBackgroundPage_.bind(this),
564 [], 'initBackgroundPage');
565 this.initializeQueue_.add(this.initPreferences_.bind(this),
566 ['initGeneral'], 'initPreferences');
567 this.initializeQueue_.add(this.initVolumeManager_.bind(this),
568 ['initGeneral', 'initBackgroundPage'],
569 'initVolumeManager');
571 this.initializeQueue_.run();
574 FileManager.prototype.initializeUI = function(dialogDom, callback) {
575 this.dialogDom_ = dialogDom;
576 this.document_ = this.dialogDom_.ownerDocument;
578 this.initializeQueue_.add(
579 this.initEssentialUI_.bind(this),
580 ['initGeneral', 'initBackgroundPage'],
582 this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
583 ['initEssentialUI'], 'initAdditionalUI');
584 this.initializeQueue_.add(
585 this.initFileSystemUI_.bind(this),
586 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI');
588 // Run again just in case if all pending closures have completed and the
589 // queue has stopped and monitor the completion.
590 this.initializeQueue_.run(callback);
594 * Initializes general purpose basic things, which are used by other
595 * initializing methods.
597 * @param {function()} callback Completion callback.
600 FileManager.prototype.initGeneral_ = function(callback) {
601 // Initialize the application state.
602 // TODO(mtomasz): Unify window.appState with location.search format.
603 if (window.appState) {
604 this.params_ = window.appState.params || {};
605 this.initCurrentDirectoryURL_ = window.appState.currentDirectoryURL;
606 this.initSelectionURL_ = window.appState.selectionURL;
607 this.initTargetName_ = window.appState.targetName;
609 // Used by the select dialog only.
610 this.params_ = location.search ?
611 JSON.parse(decodeURIComponent(location.search.substr(1))) :
613 this.initCurrentDirectoryURL_ = this.params_.currentDirectoryURL;
614 this.initSelectionURL_ = this.params_.selectionURL;
615 this.initTargetName_ = this.params_.targetName;
618 // Initialize the member variables that depend this.params_.
619 this.dialogType = this.params_.type || DialogType.FULL_PAGE;
620 this.startupPrefName_ = 'file-manager-' + this.dialogType;
621 this.fileTypes_ = this.params_.typeList || [];
627 * Initialize the background page.
628 * @param {function()} callback Completion callback.
631 FileManager.prototype.initBackgroundPage_ = function(callback) {
632 chrome.runtime.getBackgroundPage(function(backgroundPage) {
633 this.backgroundPage_ = backgroundPage;
634 this.backgroundPage_.background.ready(function() {
635 loadTimeData.data = this.backgroundPage_.background.stringData;
642 * Initializes the VolumeManager instance.
643 * @param {function()} callback Completion callback.
646 FileManager.prototype.initVolumeManager_ = function(callback) {
647 // Auto resolving to local path does not work for folders (e.g., dialog for
648 // loading unpacked extensions).
649 var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type);
651 // If this condition is false, VolumeManagerWrapper hides all drive
652 // related event and data, even if Drive is enabled on preference.
653 // In other words, even if Drive is disabled on preference but Files.app
654 // should show Drive when it is re-enabled, then the value should be set to
656 // Note that the Drive enabling preference change is listened by
657 // DriveIntegrationService, so here we don't need to take care about it.
659 !noLocalPathResolution || !this.params_.shouldReturnLocalPath;
660 this.volumeManager_ = new VolumeManagerWrapper(
661 driveEnabled, this.backgroundPage_);
666 * One time initialization of the Files.app's essential UI elements. These
667 * elements will be shown to the user. Only visible elements should be
668 * initialized here. Any heavy operation should be avoided. Files.app's
669 * window is shown at the end of this routine.
671 * @param {function()} callback Completion callback.
674 FileManager.prototype.initEssentialUI_ = function(callback) {
675 // Record stats of dialog types. New values must NOT be inserted into the
676 // array enumerating the types. It must be in sync with
677 // FileDialogType enum in tools/metrics/histograms/histogram.xml.
678 metrics.recordEnum('Create', this.dialogType,
679 [DialogType.SELECT_FOLDER,
680 DialogType.SELECT_UPLOAD_FOLDER,
681 DialogType.SELECT_SAVEAS_FILE,
682 DialogType.SELECT_OPEN_FILE,
683 DialogType.SELECT_OPEN_MULTI_FILE,
684 DialogType.FULL_PAGE]);
686 // Create the metadata cache.
687 this.metadataCache_ = MetadataCache.createFull(this.volumeManager_);
689 // Create the root view of FileManager.
690 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType);
691 this.fileTypeSelector_ = this.ui_.fileTypeSelector;
692 this.okButton_ = this.ui_.okButton;
693 this.cancelButton_ = this.ui_.cancelButton;
695 // Show the window as soon as the UI pre-initialization is done.
696 if (this.dialogType == DialogType.FULL_PAGE &&
697 !util.platform.runningInBrowser()) {
698 chrome.app.window.current().show();
699 setTimeout(callback, 100); // Wait until the animation is finished.
706 * One-time initialization of dialogs.
709 FileManager.prototype.initDialogs_ = function() {
710 // Initialize the dialog.
711 this.ui_.initDialogs();
712 FileManagerDialogBase.setFileManager(this);
714 // Obtains the dialog instances from FileManagerUI.
715 // TODO(hirono): Remove the properties from the FileManager class.
716 this.error = this.ui_.errorDialog;
717 this.alert = this.ui_.alertDialog;
718 this.confirm = this.ui_.confirmDialog;
719 this.prompt = this.ui_.promptDialog;
720 this.shareDialog_ = this.ui_.shareDialog;
721 this.defaultTaskPicker = this.ui_.defaultTaskPicker;
722 this.suggestAppsDialog = this.ui_.suggestAppsDialog;
726 * One-time initialization of various DOM nodes. Loads the additional DOM
727 * elements visible to the user. Initialize here elements, which are expensive
728 * or hidden in the beginning.
730 * @param {function()} callback Completion callback.
733 FileManager.prototype.initAdditionalUI_ = function(callback) {
735 this.ui_.initAdditionalUI();
737 this.dialogDom_.addEventListener('drop', function(e) {
738 // Prevent opening an URL by dropping it onto the page.
742 this.dialogDom_.addEventListener('click',
743 this.onExternalLinkClick_.bind(this));
744 // Cache nodes we'll be manipulating.
745 var dom = this.dialogDom_;
747 this.filenameInput_ = dom.querySelector('#filename-input-box input');
748 this.taskItems_ = dom.querySelector('#tasks');
750 this.table_ = dom.querySelector('.detail-table');
751 this.grid_ = dom.querySelector('.thumbnail-grid');
752 this.spinner_ = dom.querySelector('#list-container > .spinner-layer');
753 this.showSpinner_(true);
755 // Check the option to hide the selecting checkboxes.
756 this.table_.showCheckboxes = this.showCheckboxes_;
758 var fullPage = this.dialogType == DialogType.FULL_PAGE;
759 FileTable.decorate(this.table_, this.metadataCache_, fullPage);
760 FileGrid.decorate(this.grid_, this.metadataCache_, this.volumeManager_);
762 this.previewPanel_ = new PreviewPanel(
763 dom.querySelector('.preview-panel'),
764 DialogType.isOpenDialog(this.dialogType) ?
765 PreviewPanel.VisibilityType.ALWAYS_VISIBLE :
766 PreviewPanel.VisibilityType.AUTO,
768 this.volumeManager_);
769 this.previewPanel_.addEventListener(
770 PreviewPanel.Event.VISIBILITY_CHANGE,
771 this.onPreviewPanelVisibilityChange_.bind(this));
772 this.previewPanel_.initialize();
774 this.previewPanel_.breadcrumbs.addEventListener(
775 'pathclick', this.onBreadcrumbClick_.bind(this));
777 // Initialize progress center panel.
778 this.progressCenterPanel_ = new ProgressCenterPanel(
779 dom.querySelector('#progress-center'));
780 this.backgroundPage_.background.progressCenter.addPanel(
781 this.progressCenterPanel_);
783 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
785 // This capturing event is only used to distinguish focusing using
786 // keyboard from focusing using mouse.
787 this.document_.addEventListener('mousedown', function() {
788 this.suppressFocus_ = true;
791 this.renameInput_ = this.document_.createElement('input');
792 this.renameInput_.className = 'rename';
794 this.renameInput_.addEventListener(
795 'keydown', this.onRenameInputKeyDown_.bind(this));
796 this.renameInput_.addEventListener(
797 'blur', this.onRenameInputBlur_.bind(this));
799 // TODO(hirono): Rename the handler after creating the DialogFooter class.
800 this.filenameInput_.addEventListener(
801 'input', this.onFilenameInputInput_.bind(this));
802 this.filenameInput_.addEventListener(
803 'keydown', this.onFilenameInputKeyDown_.bind(this));
804 this.filenameInput_.addEventListener(
805 'focus', this.onFilenameInputFocus_.bind(this));
807 this.listContainer_ = this.dialogDom_.querySelector('#list-container');
808 this.listContainer_.addEventListener(
809 'keydown', this.onListKeyDown_.bind(this));
810 this.listContainer_.addEventListener(
811 'keypress', this.onListKeyPress_.bind(this));
812 this.listContainer_.addEventListener(
813 'mousemove', this.onListMouseMove_.bind(this));
815 this.okButton_.addEventListener('click', this.onOk_.bind(this));
816 this.onCancelBound_ = this.onCancel_.bind(this);
817 this.cancelButton_.addEventListener('click', this.onCancelBound_);
819 this.decorateSplitter(
820 this.dialogDom_.querySelector('#navigation-list-splitter'));
821 this.decorateSplitter(
822 this.dialogDom_.querySelector('#middlebar-splitter'));
824 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
826 this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings');
827 this.syncButton.addEventListener('click', this.onDrivePrefClick_.bind(
828 this, 'cellularDisabled', false /* not inverted */));
830 this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings');
831 this.hostedButton.addEventListener('click', this.onDrivePrefClick_.bind(
832 this, 'hostedFilesDisabled', true /* inverted */));
834 this.detailViewButton_ =
835 this.dialogDom_.querySelector('#detail-view');
836 this.detailViewButton_.addEventListener('activate',
837 this.onDetailViewButtonClick_.bind(this));
839 this.thumbnailViewButton_ =
840 this.dialogDom_.querySelector('#thumbnail-view');
841 this.thumbnailViewButton_.addEventListener('activate',
842 this.onThumbnailViewButtonClick_.bind(this));
844 cr.ui.ComboButton.decorate(this.taskItems_);
845 this.taskItems_.showMenu = function(shouldSetFocus) {
846 // Prevent the empty menu from opening.
847 if (!this.menu.length)
849 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus);
851 this.taskItems_.addEventListener('select',
852 this.onTaskItemClicked_.bind(this));
854 this.dialogDom_.ownerDocument.defaultView.addEventListener(
855 'resize', this.onResize_.bind(this));
857 this.filePopup_ = null;
859 this.searchBoxWrapper_ = this.ui_.searchBox.element;
860 this.searchBox_ = this.ui_.searchBox.inputElement;
861 this.searchBox_.addEventListener(
862 'input', this.onSearchBoxUpdate_.bind(this));
863 this.ui_.searchBox.clearButton.addEventListener(
864 'click', this.onSearchClearButtonClick_.bind(this));
866 this.lastSearchQuery_ = '';
868 this.autocompleteList_ = this.ui_.searchBox.autocompleteList;
869 this.autocompleteList_.requestSuggestions =
870 this.requestAutocompleteSuggestions_.bind(this);
872 // Instead, open the suggested item when Enter key is pressed or
874 this.autocompleteList_.handleEnterKeydown = function(event) {
875 this.openAutocompleteSuggestion_();
876 this.lastAutocompleteQuery_ = '';
877 this.autocompleteList_.suggestions = [];
879 this.autocompleteList_.addEventListener('mousedown', function(event) {
880 this.openAutocompleteSuggestion_();
881 this.lastAutocompleteQuery_ = '';
882 this.autocompleteList_.suggestions = [];
885 this.defaultActionMenuItem_ =
886 this.dialogDom_.querySelector('#default-action');
888 this.openWithCommand_ =
889 this.dialogDom_.querySelector('#open-with');
891 this.driveBuyMoreStorageCommand_ =
892 this.dialogDom_.querySelector('#drive-buy-more-space');
894 this.defaultActionMenuItem_.addEventListener('click',
895 this.dispatchSelectionAction_.bind(this));
897 this.initFileTypeFilter_();
899 util.addIsFocusedMethod();
901 // Populate the static localized strings.
902 i18nTemplate.process(this.document_, loadTimeData);
904 // Arrange the file list.
905 this.table_.normalizeColumns();
906 this.table_.redraw();
912 * @param {Event} event Click event.
915 FileManager.prototype.onBreadcrumbClick_ = function(event) {
916 this.directoryModel_.changeDirectoryEntry(event.entry);
920 * Constructs table and grid (heavy operation).
923 FileManager.prototype.initFileList_ = function() {
924 // Always sharing the data model between the detail/thumb views confuses
925 // them. Instead we maintain this bogus data model, and hook it up to the
926 // view that is not in use.
927 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
928 this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
930 var singleSelection =
931 this.dialogType == DialogType.SELECT_OPEN_FILE ||
932 this.dialogType == DialogType.SELECT_FOLDER ||
933 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
934 this.dialogType == DialogType.SELECT_SAVEAS_FILE;
936 this.fileFilter_ = new FileFilter(
938 false /* Don't show dot files by default. */);
940 this.fileWatcher_ = new FileWatcher(this.metadataCache_);
941 this.fileWatcher_.addEventListener(
942 'watcher-metadata-changed',
943 this.onWatcherMetadataChanged_.bind(this));
945 this.directoryModel_ = new DirectoryModel(
950 this.volumeManager_);
952 this.folderShortcutsModel_ = new FolderShortcutsDataModel(
953 this.volumeManager_);
955 this.selectionHandler_ = new FileSelectionHandler(this);
957 var dataModel = this.directoryModel_.getFileList();
959 this.table_.setupCompareFunctions(dataModel);
961 dataModel.addEventListener('permuted',
962 this.updateStartupPrefs_.bind(this));
964 this.directoryModel_.getFileListSelection().addEventListener('change',
965 this.selectionHandler_.onFileSelectionChanged.bind(
966 this.selectionHandler_));
968 this.initList_(this.grid_);
969 this.initList_(this.table_.list);
971 var fileListFocusBound = this.onFileListFocus_.bind(this);
972 var fileListBlurBound = this.onFileListBlur_.bind(this);
974 this.table_.list.addEventListener('focus', fileListFocusBound);
975 this.grid_.addEventListener('focus', fileListFocusBound);
977 this.table_.list.addEventListener('blur', fileListBlurBound);
978 this.grid_.addEventListener('blur', fileListBlurBound);
980 var dragStartBound = this.onDragStart_.bind(this);
981 this.table_.list.addEventListener('dragstart', dragStartBound);
982 this.grid_.addEventListener('dragstart', dragStartBound);
984 var dragEndBound = this.onDragEnd_.bind(this);
985 this.table_.list.addEventListener('dragend', dragEndBound);
986 this.grid_.addEventListener('dragend', dragEndBound);
987 // This event is published by DragSelector because drag end event is not
988 // published at the end of drag selection.
989 this.table_.list.addEventListener('dragselectionend', dragEndBound);
990 this.grid_.addEventListener('dragselectionend', dragEndBound);
992 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
993 // attach the directory model.
994 this.initNavigationList_();
996 this.table_.addEventListener('column-resize-end',
997 this.updateStartupPrefs_.bind(this));
999 // Restore preferences.
1000 this.directoryModel_.getFileList().sort(
1001 this.viewOptions_.sortField || 'modificationTime',
1002 this.viewOptions_.sortDirection || 'desc');
1003 if (this.viewOptions_.columns) {
1004 var cm = this.table_.columnModel;
1005 for (var i = 0; i < cm.totalSize; i++) {
1006 if (this.viewOptions_.columns[i] > 0)
1007 cm.setWidth(i, this.viewOptions_.columns[i]);
1010 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
1012 this.textSearchState_ = {text: '', date: new Date()};
1013 this.closeOnUnmount_ = (this.params_.action == 'auto-open');
1015 if (this.closeOnUnmount_) {
1016 this.volumeManager_.addEventListener('externally-unmounted',
1017 this.onExternallyUnmounted_.bind(this));
1020 // Update metadata to change 'Today' and 'Yesterday' dates.
1021 var today = new Date();
1023 today.setMinutes(0);
1024 today.setSeconds(0);
1025 today.setMilliseconds(0);
1026 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1027 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
1033 FileManager.prototype.initNavigationList_ = function() {
1034 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
1035 DirectoryTree.decorate(this.directoryTree_,
1036 this.directoryModel_,
1037 this.volumeManager_);
1039 this.navigationList_ = this.dialogDom_.querySelector('#navigation-list');
1040 NavigationList.decorate(this.navigationList_,
1041 this.volumeManager_,
1042 this.directoryModel_);
1043 this.navigationList_.fileManager = this;
1044 this.navigationList_.dataModel = new NavigationListModel(
1045 this.volumeManager_, this.folderShortcutsModel_);
1051 FileManager.prototype.updateMiddleBarVisibility_ = function() {
1052 var entry = this.directoryModel_.getCurrentDirEntry();
1056 var driveVolume = this.volumeManager_.getVolumeInfo(entry);
1057 var visible = driveVolume && !driveVolume.error &&
1058 driveVolume.volumeType === util.VolumeType.DRIVE;
1060 querySelector('.dialog-middlebar-contents').hidden = !visible;
1061 this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible;
1068 FileManager.prototype.updateStartupPrefs_ = function() {
1069 var sortStatus = this.directoryModel_.getFileList().sortStatus;
1071 sortField: sortStatus.field,
1072 sortDirection: sortStatus.direction,
1074 listType: this.listType_
1076 var cm = this.table_.columnModel;
1077 for (var i = 0; i < cm.totalSize; i++) {
1078 prefs.columns.push(cm.getWidth(i));
1080 // Save the global default.
1081 util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs));
1083 // Save the window-specific preference.
1084 if (window.appState) {
1085 window.appState.viewOptions = prefs;
1086 util.saveAppState();
1090 FileManager.prototype.refocus = function() {
1092 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
1093 targetElement = this.filenameInput_;
1095 targetElement = this.currentList_;
1097 // Hack: if the tabIndex is disabled, we can assume a modal dialog is
1098 // shown. Focus to a button on the dialog instead.
1099 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1)
1100 targetElement = document.querySelector('button:not([tabIndex="-1"])');
1103 targetElement.focus();
1107 * File list focus handler. Used to select the top most element on the list
1108 * if nothing was selected.
1112 FileManager.prototype.onFileListFocus_ = function() {
1113 // Do not select default item if focused using mouse.
1114 if (this.suppressFocus_)
1117 var selection = this.getSelection();
1118 if (!selection || selection.totalCount != 0)
1121 this.directoryModel_.selectIndex(0);
1125 * File list blur handler.
1129 FileManager.prototype.onFileListBlur_ = function() {
1130 this.suppressFocus_ = false;
1134 * Index of selected item in the typeList of the dialog params.
1136 * @return {number} 1-based index of selected type or 0 if no type selected.
1139 FileManager.prototype.getSelectedFilterIndex_ = function() {
1140 var index = Number(this.fileTypeSelector_.selectedIndex);
1141 if (index < 0) // Nothing selected.
1143 if (this.params_.includeAllFiles) // Already 1-based.
1145 return index + 1; // Convert to 1-based;
1148 FileManager.prototype.setListType = function(type) {
1149 if (type && type == this.listType_)
1152 this.table_.list.startBatchUpdates();
1153 this.grid_.startBatchUpdates();
1155 // TODO(dzvorygin): style.display and dataModel setting order shouldn't
1156 // cause any UI bugs. Currently, the only right way is first to set display
1157 // style and only then set dataModel.
1159 if (type == FileManager.ListType.DETAIL) {
1160 this.table_.dataModel = this.directoryModel_.getFileList();
1161 this.table_.selectionModel = this.directoryModel_.getFileListSelection();
1162 this.table_.hidden = false;
1163 this.grid_.hidden = true;
1164 this.grid_.selectionModel = this.emptySelectionModel_;
1165 this.grid_.dataModel = this.emptyDataModel_;
1166 this.table_.hidden = false;
1167 /** @type {cr.ui.List} */
1168 this.currentList_ = this.table_.list;
1169 this.detailViewButton_.setAttribute('checked', '');
1170 this.thumbnailViewButton_.removeAttribute('checked');
1171 this.detailViewButton_.setAttribute('disabled', '');
1172 this.thumbnailViewButton_.removeAttribute('disabled');
1173 } else if (type == FileManager.ListType.THUMBNAIL) {
1174 this.grid_.dataModel = this.directoryModel_.getFileList();
1175 this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
1176 this.grid_.hidden = false;
1177 this.table_.hidden = true;
1178 this.table_.selectionModel = this.emptySelectionModel_;
1179 this.table_.dataModel = this.emptyDataModel_;
1180 this.grid_.hidden = false;
1181 /** @type {cr.ui.List} */
1182 this.currentList_ = this.grid_;
1183 this.thumbnailViewButton_.setAttribute('checked', '');
1184 this.detailViewButton_.removeAttribute('checked');
1185 this.thumbnailViewButton_.setAttribute('disabled', '');
1186 this.detailViewButton_.removeAttribute('disabled');
1188 throw new Error('Unknown list type: ' + type);
1191 this.listType_ = type;
1192 this.updateStartupPrefs_();
1195 this.table_.list.endBatchUpdates();
1196 this.grid_.endBatchUpdates();
1200 * Initialize the file list table or grid.
1202 * @param {cr.ui.List} list The list.
1205 FileManager.prototype.initList_ = function(list) {
1206 // Overriding the default role 'list' to 'listbox' for better accessibility
1208 list.setAttribute('role', 'listbox');
1209 list.addEventListener('click', this.onDetailClick_.bind(this));
1210 list.id = 'file-list';
1216 FileManager.prototype.onCopyProgress_ = function(event) {
1217 if (event.reason == 'ERROR' &&
1218 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
1219 event.error.data.toDrive &&
1220 event.error.data.name == util.FileError.QUOTA_EXCEEDED_ERR) {
1221 this.alert.showHtml(
1222 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
1223 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
1225 event.error.data.sourceFileUrl.split('/').pop()),
1226 str('GOOGLE_DRIVE_BUY_STORAGE_URL')));
1231 * Handler of file manager operations. Called when an entry has been
1233 * This updates directory model to reflect operation result immediately (not
1234 * waiting for directory update event). Also, preloads thumbnails for the
1235 * images of new entries.
1236 * See also FileOperationManager.EventRouter.
1238 * @param {Event} event An event for the entry change.
1241 FileManager.prototype.onEntryChanged_ = function(event) {
1242 var kind = event.kind;
1243 var entry = event.entry;
1244 this.directoryModel_.onEntryChanged(kind, entry);
1245 this.selectionHandler_.onFileSelectionChanged();
1247 if (kind === util.EntryChangedKind.CREATED && FileType.isImage(entry)) {
1248 // Preload a thumbnail if the new copied entry an image.
1249 var locationInfo = this.volumeManager_.getLocationInfo(entry);
1252 this.metadataCache_.get(entry, 'thumbnail|drive', function(metadata) {
1253 var thumbnailLoader_ = new ThumbnailLoader(
1255 ThumbnailLoader.LoaderType.CANVAS,
1257 undefined, // Media type.
1258 // TODO(mtomasz): Use Entry instead of paths.
1259 locationInfo.isDriveBased ?
1260 ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1261 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1262 10); // Very low priority.
1263 thumbnailLoader_.loadDetachedImage(function(success) {});
1269 * Fills the file type list or hides it.
1272 FileManager.prototype.initFileTypeFilter_ = function() {
1273 if (this.params_.includeAllFiles) {
1274 var option = this.document_.createElement('option');
1275 option.innerText = str('ALL_FILES_FILTER');
1276 this.fileTypeSelector_.appendChild(option);
1280 for (var i = 0; i !== this.fileTypes_.length; i++) {
1281 var fileType = this.fileTypes_[i];
1282 var option = this.document_.createElement('option');
1283 var description = fileType.description;
1285 // See if all the extensions in the group have the same description.
1286 for (var j = 0; j !== fileType.extensions.length; j++) {
1287 var currentDescription = FileType.typeToString(
1288 FileType.getTypeForName('.' + fileType.extensions[j]));
1289 if (!description) // Set the first time.
1290 description = currentDescription;
1291 else if (description != currentDescription) {
1292 // No single description, fall through to the extension list.
1299 // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1300 description = fileType.extensions.map(function(s) {
1304 option.innerText = description;
1306 option.value = i + 1;
1308 if (fileType.selected)
1309 option.selected = true;
1311 this.fileTypeSelector_.appendChild(option);
1314 var options = this.fileTypeSelector_.querySelectorAll('option');
1315 if (options.length >= 2) {
1316 // There is in fact no choice, show the selector.
1317 this.fileTypeSelector_.hidden = false;
1319 this.fileTypeSelector_.addEventListener('change',
1320 this.updateFileTypeFilter_.bind(this));
1325 * Filters file according to the selected file type.
1328 FileManager.prototype.updateFileTypeFilter_ = function() {
1329 this.fileFilter_.removeFilter('fileType');
1330 var selectedIndex = this.getSelectedFilterIndex_();
1331 if (selectedIndex > 0) { // Specific filter selected.
1332 var regexp = new RegExp('.*(' +
1333 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
1334 var filter = function(entry) {
1335 return entry.isDirectory || regexp.test(entry.name);
1337 this.fileFilter_.addFilter('fileType', filter);
1342 * Resize details and thumb views to fit the new window size.
1345 FileManager.prototype.onResize_ = function() {
1346 if (this.listType_ == FileManager.ListType.THUMBNAIL)
1347 this.grid_.relayout();
1349 this.table_.relayout();
1351 // May not be available during initialization.
1352 if (this.directoryTree_)
1353 this.directoryTree_.relayout();
1355 // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before
1356 // file system is available.
1357 if (this.navigationList_)
1358 this.navigationList_.redraw();
1360 this.ui_.searchBox.updateSizeRelatedStyle();
1362 this.previewPanel_.breadcrumbs.truncate();
1366 * Handles local metadata changes in the currect directory.
1367 * @param {Event} event Change event.
1370 FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
1371 this.updateMetadataInUI_(
1372 event.metadataType, event.entries, event.properties);
1376 * Resize details and thumb views to fit the new window size.
1379 FileManager.prototype.onPreviewPanelVisibilityChange_ = function() {
1380 // This method may be called on initialization. Some object may not be
1383 var panelHeight = this.previewPanel_.visible ?
1384 this.previewPanel_.height : 0;
1386 this.grid_.setBottomMarginForPanel(panelHeight);
1388 this.table_.setBottomMarginForPanel(panelHeight);
1390 if (this.directoryTree_) {
1391 this.directoryTree_.setBottomMarginForPanel(panelHeight);
1392 this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
1397 * Invoked when the drag is started on the list or the grid.
1400 FileManager.prototype.onDragStart_ = function() {
1401 // On open file dialog, the preview panel is always shown.
1402 if (DialogType.isOpenDialog(this.dialogType))
1404 this.previewPanel_.visibilityType =
1405 PreviewPanel.VisibilityType.ALWAYS_HIDDEN;
1409 * Invoked when the drag is ended on the list or the grid.
1412 FileManager.prototype.onDragEnd_ = function() {
1413 // On open file dialog, the preview panel is always shown.
1414 if (DialogType.isOpenDialog(this.dialogType))
1416 this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO;
1420 * Sets up the current directory during initialization.
1423 FileManager.prototype.setupCurrentDirectory_ = function() {
1424 var tracker = this.directoryModel_.createDirectoryChangeTracker();
1425 var queue = new AsyncUtil.Queue();
1427 // Wait until the volume manager is initialized.
1428 queue.run(function(callback) {
1430 this.volumeManager_.ensureInitialized(callback);
1433 var nextCurrentDirEntry;
1436 // Resolve the selectionURL to selectionEntry or to currentDirectoryEntry
1437 // in case of being a display root.
1438 queue.run(function(callback) {
1439 if (!this.initSelectionURL_) {
1443 webkitResolveLocalFileSystemURL(
1444 this.initSelectionURL_,
1446 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1447 // If location information is not available, then the volume is
1448 // no longer (or never) available.
1449 if (!locationInfo) {
1453 // If the selection is root, then use it as a current directory
1454 // instead. This is because, selecting a root entry is done as
1456 if (locationInfo.isRootEntry)
1457 nextCurrentDirEntry = inEntry;
1459 selectionEntry = inEntry;
1461 }.bind(this), callback);
1463 // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done
1464 // by the previous step).
1465 queue.run(function(callback) {
1466 if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) {
1470 webkitResolveLocalFileSystemURL(
1471 this.initCurrentDirectoryURL_,
1473 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1474 if (!locationInfo) {
1478 nextCurrentDirEntry = inEntry;
1480 }.bind(this), callback);
1481 // TODO(mtomasz): Implement reopening on special search, when fake
1482 // entries are converted to directory providers.
1485 // If the directory to be changed to is not available, then first fallback
1486 // to the parent of the selection entry.
1487 queue.run(function(callback) {
1488 if (nextCurrentDirEntry || !selectionEntry) {
1492 selectionEntry.getParent(function(inEntry) {
1493 nextCurrentDirEntry = inEntry;
1498 // If the directory to be changed to is still not resolved, then fallback
1499 // to the default display root.
1500 queue.run(function(callback) {
1501 if (nextCurrentDirEntry) {
1505 this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) {
1506 nextCurrentDirEntry = displayRoot;
1511 // If selection failed to be resolved (eg. didn't exist, in case of saving
1512 // a file, or in case of a fallback of the current directory, then try to
1513 // resolve again using the target name.
1514 queue.run(function(callback) {
1515 if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) {
1519 // Try to resolve as a file first. If it fails, then as a directory.
1520 nextCurrentDirEntry.getFile(
1521 this.initTargetName_,
1523 function(targetEntry) {
1524 selectionEntry = targetEntry;
1527 // Failed to resolve as a file
1528 nextCurrentDirEntry.getDirectory(
1529 this.initTargetName_,
1531 function(targetEntry) {
1532 selectionEntry = targetEntry;
1535 // Failed to resolve as either file or directory.
1543 queue.run(function(callback) {
1544 // Check directory change.
1546 if (tracker.hasChanged) {
1550 // Finish setup current directory.
1551 this.finishSetupCurrentDirectory_(
1552 nextCurrentDirEntry,
1554 this.initTargetName_);
1560 * @param {DirectoryEntry} directoryEntry Directory to be opened.
1561 * @param {Entry=} opt_selectionEntry Entry to be selected.
1562 * @param {string=} opt_suggestedName Suggested name for a non-existing\
1566 FileManager.prototype.finishSetupCurrentDirectory_ = function(
1567 directoryEntry, opt_selectionEntry, opt_suggestedName) {
1568 // Open the directory, and select the selection (if passed).
1569 if (util.isFakeEntry(directoryEntry)) {
1570 this.directoryModel_.specialSearch(directoryEntry, '');
1572 this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
1573 if (opt_selectionEntry)
1574 this.directoryModel_.selectEntry(opt_selectionEntry);
1578 if (this.dialogType === DialogType.FULL_PAGE) {
1579 // In the FULL_PAGE mode if the restored URL points to a file we might
1580 // have to invoke a task after selecting it.
1581 if (this.params_.action === 'select')
1585 // Handle restoring after crash, or the gallery action.
1586 // TODO(mtomasz): Use the gallery action instead of just the gallery
1588 if (this.params_.gallery || this.params_.action === 'gallery') {
1589 if (!opt_selectionEntry) {
1590 // Non-existent file or a directory.
1591 // Reloading while the Gallery is open with empty or multiple
1592 // selection. Open the Gallery when the directory is scanned.
1594 new FileTasks(this, this.params_).openGallery([]);
1597 // The file or the directory exists.
1599 new FileTasks(this, this.params_).openGallery([opt_selectionEntry]);
1603 // TODO(mtomasz): Implement remounting archives after crash.
1604 // See: crbug.com/333139
1607 // If there is a task to be run, run it after the scan is completed.
1609 var listener = function() {
1610 if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(),
1612 // Opened on a different URL. Probably fallbacked. Therefore,
1613 // do not invoke a task.
1616 this.directoryModel_.removeEventListener(
1617 'scan-completed', listener);
1620 this.directoryModel_.addEventListener('scan-completed', listener);
1622 } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
1623 this.filenameInput_.value = opt_suggestedName || '';
1624 this.selectTargetNameInFilenameInput_();
1631 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
1632 var entries = this.directoryModel_.getFileList().slice();
1633 var directoryEntry = this.directoryModel_.getCurrentDirEntry();
1634 if (!directoryEntry)
1636 // We don't pass callback here. When new metadata arrives, we have an
1637 // observer registered to update the UI.
1639 // TODO(dgozman): refresh content metadata only when modificationTime
1641 var isFakeEntry = util.isFakeEntry(directoryEntry);
1642 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
1644 this.metadataCache_.clearRecursively(directoryEntry, '*');
1645 this.metadataCache_.get(getEntries, 'filesystem', null);
1647 if (this.isOnDrive())
1648 this.metadataCache_.get(getEntries, 'drive', null);
1650 var visibleItems = this.currentList_.items;
1651 var visibleEntries = [];
1652 for (var i = 0; i < visibleItems.length; i++) {
1653 var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
1654 var entry = this.directoryModel_.getFileList().item(index);
1655 // The following check is a workaround for the bug in list: sometimes item
1656 // does not have listIndex, and therefore is not found in the list.
1657 if (entry) visibleEntries.push(entry);
1659 this.metadataCache_.get(visibleEntries, 'thumbnail', null);
1665 FileManager.prototype.dailyUpdateModificationTime_ = function() {
1666 var entries = this.directoryModel_.getFileList().slice();
1667 this.metadataCache_.get(
1670 this.updateMetadataInUI_.bind(this, 'filesystem', entries));
1672 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1673 MILLISECONDS_IN_DAY);
1677 * @param {string} type Type of metadata changed.
1678 * @param {Array.<Entry>} entries Array of entries.
1679 * @param {Object.<string, Object>} props Map from entry URLs to metadata
1683 FileManager.prototype.updateMetadataInUI_ = function(
1684 type, entries, properties) {
1685 if (this.listType_ == FileManager.ListType.DETAIL)
1686 this.table_.updateListItemsMetadata(type, properties);
1688 this.grid_.updateListItemsMetadata(type, properties);
1689 // TODO: update bottom panel thumbnails.
1693 * Restore the item which is being renamed while refreshing the file list. Do
1694 * nothing if no item is being renamed or such an item disappeared.
1696 * While refreshing file list it gets repopulated with new file entries.
1697 * There is not a big difference whether DOM items stay the same or not.
1698 * Except for the item that the user is renaming.
1702 FileManager.prototype.restoreItemBeingRenamed_ = function() {
1703 if (!this.isRenamingInProgress())
1706 var dm = this.directoryModel_;
1707 var leadIndex = dm.getFileListSelection().leadIndex;
1711 var leadEntry = dm.getFileList().item(leadIndex);
1712 if (!util.isSameEntry(this.renameInput_.currentEntry, leadEntry))
1715 var leadListItem = this.findListItemForNode_(this.renameInput_);
1716 if (this.currentList_ == this.table_.list) {
1717 this.table_.updateFileMetadata(leadListItem, leadEntry);
1719 this.currentList_.restoreLeadItem(leadListItem);
1723 * TODO(mtomasz): Move this to a utility function working on the root type.
1724 * @return {boolean} True if the current directory content is from Google
1727 FileManager.prototype.isOnDrive = function() {
1728 var rootType = this.directoryModel_.getCurrentRootType();
1729 return rootType === RootType.DRIVE ||
1730 rootType === RootType.DRIVE_SHARED_WITH_ME ||
1731 rootType === RootType.DRIVE_RECENT ||
1732 rootType === RootType.DRIVE_OFFLINE;
1736 * Overrides default handling for clicks on hyperlinks.
1737 * In a packaged apps links with targer='_blank' open in a new tab by
1738 * default, other links do not open at all.
1740 * @param {Event} event Click event.
1743 FileManager.prototype.onExternalLinkClick_ = function(event) {
1744 if (event.target.tagName != 'A' || !event.target.href)
1747 if (this.dialogType != DialogType.FULL_PAGE)
1752 * Task combobox handler.
1754 * @param {Object} event Event containing task which was clicked.
1757 FileManager.prototype.onTaskItemClicked_ = function(event) {
1758 var selection = this.getSelection();
1759 if (!selection.tasks) return;
1761 if (event.item.task) {
1762 // Task field doesn't exist on change-default dropdown item.
1763 selection.tasks.execute(event.item.task.taskId);
1765 var extensions = [];
1767 for (var i = 0; i < selection.entries.length; i++) {
1768 var match = /\.(\w+)$/g.exec(selection.entries[i].toURL());
1770 var ext = match[1].toUpperCase();
1771 if (extensions.indexOf(ext) == -1) {
1772 extensions.push(ext);
1779 if (extensions.length == 1) {
1780 format = extensions[0];
1783 // Change default was clicked. We should open "change default" dialog.
1784 selection.tasks.showTaskPicker(this.defaultTaskPicker,
1785 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
1786 strf('CHANGE_DEFAULT_CAPTION', format),
1787 this.onDefaultTaskDone_.bind(this));
1792 * Sets the given task as default, when this task is applicable.
1794 * @param {Object} task Task to set as default.
1797 FileManager.prototype.onDefaultTaskDone_ = function(task) {
1798 // TODO(dgozman): move this method closer to tasks.
1799 var selection = this.getSelection();
1800 chrome.fileBrowserPrivate.setDefaultTask(
1802 util.entriesToURLs(selection.entries),
1803 selection.mimeTypes);
1804 selection.tasks = new FileTasks(this);
1805 selection.tasks.init(selection.entries, selection.mimeTypes);
1806 selection.tasks.display(this.taskItems_);
1807 this.refreshCurrentDirectoryMetadata_();
1808 this.selectionHandler_.onFileSelectionChanged();
1814 FileManager.prototype.onPreferencesChanged_ = function() {
1816 this.getPreferences_(function(prefs) {
1817 self.initDateTimeFormatters_();
1818 self.refreshCurrentDirectoryMetadata_();
1820 if (prefs.cellularDisabled)
1821 self.syncButton.setAttribute('checked', '');
1823 self.syncButton.removeAttribute('checked');
1825 if (self.hostedButton.hasAttribute('checked') !=
1826 prefs.hostedFilesDisabled && self.isOnDrive()) {
1827 self.directoryModel_.rescan();
1830 if (!prefs.hostedFilesDisabled)
1831 self.hostedButton.setAttribute('checked', '');
1833 self.hostedButton.removeAttribute('checked');
1835 true /* refresh */);
1838 FileManager.prototype.onDriveConnectionChanged_ = function() {
1839 var connection = this.volumeManager_.getDriveConnectionState();
1840 if (this.commandHandler)
1841 this.commandHandler.updateAvailability();
1842 if (this.dialogContainer_)
1843 this.dialogContainer_.setAttribute('connection', connection.type);
1844 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
1845 this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
1849 * Tells whether the current directory is read only.
1850 * TODO(mtomasz): Remove and use EntryLocation directly.
1851 * @return {boolean} True if read only, false otherwise.
1853 FileManager.prototype.isOnReadonlyDirectory = function() {
1854 return this.directoryModel_.isReadOnly();
1858 * @param {Event} Unmount event.
1861 FileManager.prototype.onExternallyUnmounted_ = function(event) {
1862 if (event.volumeInfo === this.currentVolumeInfo_) {
1863 if (this.closeOnUnmount_) {
1864 // If the file manager opened automatically when a usb drive inserted,
1865 // user have never changed current volume (that implies the current
1866 // directory is still on the device) then close this window.
1873 * Shows a modal-like file viewer/editor on top of the File Manager UI.
1875 * @param {HTMLElement} popup Popup element.
1876 * @param {function()} closeCallback Function to call after the popup is
1879 FileManager.prototype.openFilePopup = function(popup, closeCallback) {
1880 this.closeFilePopup();
1881 this.filePopup_ = popup;
1882 this.filePopupCloseCallback_ = closeCallback;
1883 this.dialogDom_.insertBefore(
1884 this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area'));
1885 this.filePopup_.focus();
1886 this.document_.body.setAttribute('overlay-visible', '');
1887 this.document_.querySelector('#iframe-drag-area').hidden = false;
1891 * Closes the modal-like file viewer/editor popup.
1893 FileManager.prototype.closeFilePopup = function() {
1894 if (this.filePopup_) {
1895 this.document_.body.removeAttribute('overlay-visible');
1896 this.document_.querySelector('#iframe-drag-area').hidden = true;
1897 // The window resize would not be processed properly while the relevant
1898 // divs had 'display:none', force resize after the layout fired.
1899 setTimeout(this.onResize_.bind(this), 0);
1900 if (this.filePopup_.contentWindow &&
1901 this.filePopup_.contentWindow.unload) {
1902 this.filePopup_.contentWindow.unload();
1905 if (this.filePopupCloseCallback_) {
1906 this.filePopupCloseCallback_();
1907 this.filePopupCloseCallback_ = null;
1910 // These operations have to be in the end, otherwise v8 crashes on an
1911 // assert. See: crbug.com/224174.
1912 this.dialogDom_.removeChild(this.filePopup_);
1913 this.filePopup_ = null;
1918 * Updates visibility of the draggable app region in the modal-like file
1921 * @param {boolean} visible True for visible, false otherwise.
1923 FileManager.prototype.onFilePopupAppRegionChanged = function(visible) {
1924 if (!this.filePopup_)
1927 this.document_.querySelector('#iframe-drag-area').hidden = !visible;
1931 * @return {Array.<Entry>} List of all entries in the current directory.
1933 FileManager.prototype.getAllEntriesInCurrentDirectory = function() {
1934 return this.directoryModel_.getFileList().slice();
1937 FileManager.prototype.isRenamingInProgress = function() {
1938 return !!this.renameInput_.currentEntry;
1944 FileManager.prototype.focusCurrentList_ = function() {
1945 if (this.listType_ == FileManager.ListType.DETAIL)
1946 this.table_.focus();
1947 else // this.listType_ == FileManager.ListType.THUMBNAIL)
1952 * Return DirectoryEntry of the current directory or null.
1953 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
1954 * null if the directory model is not ready or the current directory is
1957 FileManager.prototype.getCurrentDirectoryEntry = function() {
1958 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
1962 * Deletes the selected file and directories recursively.
1964 FileManager.prototype.deleteSelection = function() {
1965 // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364
1966 var entries = this.getSelection().entries;
1967 var message = entries.length == 1 ?
1968 strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) :
1969 strf('GALLERY_CONFIRM_DELETE_SOME', entries.length);
1970 this.confirm.show(message, function() {
1971 this.fileOperationManager_.deleteEntries(entries);
1976 * Shows the share dialog for the selected file or directory.
1978 FileManager.prototype.shareSelection = function() {
1979 var entries = this.getSelection().entries;
1980 if (entries.length != 1) {
1981 console.warn('Unable to share multiple items at once.');
1984 // Add the overlapped class to prevent the applicaiton window from
1985 // captureing mouse events.
1986 this.shareDialog_.show(entries[0], function(result) {
1987 if (result == ShareDialog.Result.NETWORK_ERROR)
1988 this.error.show(str('SHARE_ERROR'));
1993 * Creates a folder shortcut.
1994 * @param {Entry} entry A shortcut which refers to |entry| to be created.
1996 FileManager.prototype.createFolderShortcut = function(entry) {
1998 if (this.folderShortcutExists(entry))
2001 this.folderShortcutsModel_.add(entry);
2005 * Checkes if the shortcut which refers to the given folder exists or not.
2006 * @param {Entry} entry Entry of the folder to be checked.
2008 FileManager.prototype.folderShortcutExists = function(entry) {
2009 return this.folderShortcutsModel_.exists(entry);
2013 * Removes the folder shortcut.
2014 * @param {Entry} entry The shortcut which refers to |entry| is to be removed.
2016 FileManager.prototype.removeFolderShortcut = function(entry) {
2017 this.folderShortcutsModel_.remove(entry);
2021 * Blinks the selection. Used to give feedback when copying or cutting the
2024 FileManager.prototype.blinkSelection = function() {
2025 var selection = this.getSelection();
2026 if (!selection || selection.totalCount == 0)
2029 for (var i = 0; i < selection.entries.length; i++) {
2030 var selectedIndex = selection.indexes[i];
2031 var listItem = this.currentList_.getListItemByIndex(selectedIndex);
2033 this.blinkListItem_(listItem);
2038 * @param {Element} listItem List item element.
2041 FileManager.prototype.blinkListItem_ = function(listItem) {
2042 listItem.classList.add('blink');
2043 setTimeout(function() {
2044 listItem.classList.remove('blink');
2051 FileManager.prototype.selectTargetNameInFilenameInput_ = function() {
2052 var input = this.filenameInput_;
2054 var selectionEnd = input.value.lastIndexOf('.');
2055 if (selectionEnd == -1) {
2058 input.selectionStart = 0;
2059 input.selectionEnd = selectionEnd;
2064 * Handles mouse click or tap.
2066 * @param {Event} event The click event.
2069 FileManager.prototype.onDetailClick_ = function(event) {
2070 if (this.isRenamingInProgress()) {
2071 // Don't pay attention to clicks during a rename.
2075 var listItem = this.findListItemForEvent_(event);
2076 var selection = this.getSelection();
2077 if (!listItem || !listItem.selected || selection.totalCount != 1) {
2081 // React on double click, but only if both clicks hit the same item.
2082 // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2083 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
2084 this.lastClickedItem_ = listItem;
2086 if (event.detail != clickNumber)
2089 var entry = selection.entries[0];
2090 if (entry.isDirectory) {
2091 this.onDirectoryAction_(entry);
2093 this.dispatchSelectionAction_();
2100 FileManager.prototype.dispatchSelectionAction_ = function() {
2101 if (this.dialogType == DialogType.FULL_PAGE) {
2102 var selection = this.getSelection();
2103 var tasks = selection.tasks;
2104 var urls = selection.urls;
2105 var mimeTypes = selection.mimeTypes;
2107 tasks.executeDefault();
2110 if (!this.okButton_.disabled) {
2118 * Opens the suggest file dialog.
2120 * @param {Entry} entry Entry of the file.
2121 * @param {function()} onSuccess Success callback.
2122 * @param {function()} onCancelled User-cancelled callback.
2123 * @param {function()} onFailure Failure callback.
2126 FileManager.prototype.openSuggestAppsDialog =
2127 function(entry, onSuccess, onCancelled, onFailure) {
2133 this.metadataCache_.get([entry], 'drive', function(props) {
2134 if (!props || !props[0] || !props[0].contentMimeType) {
2139 var basename = entry.name;
2140 var splitted = PathUtil.splitExtension(basename);
2141 var filename = splitted[0];
2142 var extension = splitted[1];
2143 var mime = props[0].contentMimeType;
2145 // Returns with failure if the file has neither extension nor mime.
2146 if (!extension || !mime) {
2151 var onDialogClosed = function(result) {
2153 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
2156 case SuggestAppsDialog.Result.FAILED:
2164 if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
2165 this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
2167 this.suggestAppsDialog.showByExtensionAndMime(
2168 extension, mime, onDialogClosed);
2174 * Called when a dialog is shown or hidden.
2175 * @param {boolean} flag True if a dialog is shown, false if hidden. */
2176 FileManager.prototype.onDialogShownOrHidden = function(show) {
2177 // Set/unset a flag to disable dragging on the title area.
2178 this.dialogContainer_.classList.toggle('disable-header-drag', show);
2182 * Executes directory action (i.e. changes directory).
2184 * @param {DirectoryEntry} entry Directory entry to which directory should be
2188 FileManager.prototype.onDirectoryAction_ = function(entry) {
2189 return this.directoryModel_.changeDirectoryEntry(entry);
2193 * Update the window title.
2196 FileManager.prototype.updateTitle_ = function() {
2197 if (this.dialogType != DialogType.FULL_PAGE)
2200 if (!this.currentVolumeInfo_)
2203 this.document_.title = this.currentVolumeInfo_.getLabel();
2207 * Update the gear menu.
2210 FileManager.prototype.updateGearMenu_ = function() {
2211 var hideItemsForDrive = !this.isOnDrive();
2212 this.syncButton.hidden = hideItemsForDrive;
2213 this.hostedButton.hidden = hideItemsForDrive;
2214 this.document_.getElementById('drive-separator').hidden =
2216 this.refreshRemainingSpace_(true); // Show loading caption.
2220 * Update menus that move the window to the other profile's desktop.
2221 * TODO(hirono): Add the GearMenu class and make it a member of the class.
2222 * TODO(hirono): Handle the case where a profile is added while the menu is
2226 FileManager.prototype.updateVisitDesktopMenus_ = function() {
2227 var gearMenu = this.document_.querySelector('#gear-menu');
2229 this.document_.querySelector('#multi-profile-separator');
2231 // Remove existing menu items.
2233 this.document_.querySelectorAll('#gear-menu .visit-desktop');
2234 for (var i = 0; i < oldItems.length; i++) {
2235 gearMenu.removeChild(oldItems[i]);
2237 separator.hidden = true;
2239 if (this.dialogType !== DialogType.FULL_PAGE)
2242 // Obtain the profile information.
2243 chrome.fileBrowserPrivate.getProfiles(function(profiles,
2246 // Check if the menus are needed or not.
2247 var insertingPosition = separator.nextSibling;
2248 if (profiles.length === 1 && profiles[0].profileId === displayedId)
2251 separator.hidden = false;
2252 for (var i = 0; i < profiles.length; i++) {
2253 var profile = profiles[i];
2254 if (profile.profileId === displayedId)
2256 var item = this.document_.createElement('menuitem');
2257 cr.ui.MenuItem.decorate(item);
2258 gearMenu.insertBefore(item, insertingPosition);
2259 item.className = 'visit-desktop';
2260 item.label = strf('VISIT_DESKTOP_OF_USER',
2261 profile.displayName,
2263 item.addEventListener('activate', function(inProfile, event) {
2264 // Stop propagate and hide the menu manually, in order to prevent the
2265 // focus from being back to the button. (cf. http://crbug.com/248479)
2266 event.stopPropagation();
2267 this.gearButton_.hideMenu();
2268 this.gearButton_.blur();
2269 chrome.fileBrowserPrivate.visitDesktop(inProfile.profileId);
2270 }.bind(this, profile));
2276 * Refreshes space info of the current volume.
2277 * @param {boolean} showLoadingCaption Whether show loading caption or not.
2280 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
2281 if (!this.currentVolumeInfo_)
2284 var volumeSpaceInfoLabel =
2285 this.dialogDom_.querySelector('#volume-space-info-label');
2286 var volumeSpaceInnerBar =
2287 this.dialogDom_.querySelector('#volume-space-info-bar');
2288 var volumeSpaceOuterBar =
2289 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
2291 volumeSpaceInnerBar.setAttribute('pending', '');
2293 if (showLoadingCaption) {
2294 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2295 volumeSpaceInnerBar.style.width = '100%';
2298 var currentVolumeInfo = this.currentVolumeInfo_;
2299 chrome.fileBrowserPrivate.getSizeStats(
2300 currentVolumeInfo.volumeId, function(result) {
2301 var volumeInfo = this.volumeManager_.getVolumeInfo(
2302 this.directoryModel_.getCurrentDirEntry());
2303 if (currentVolumeInfo !== this.currentVolumeInfo_)
2305 updateSpaceInfo(result,
2306 volumeSpaceInnerBar,
2307 volumeSpaceInfoLabel,
2308 volumeSpaceOuterBar);
2313 * Update the UI when the current directory changes.
2315 * @param {Event} event The directory-changed event.
2318 FileManager.prototype.onDirectoryChanged_ = function(event) {
2319 var newCurrentVolumeInfo = this.volumeManager_.getVolumeInfo(
2322 // If volume has changed, then update the gear menu.
2323 if (this.currentVolumeInfo_ !== newCurrentVolumeInfo) {
2324 this.updateGearMenu_();
2325 // If the volume has changed, and it was previously set, then do not
2326 // close on unmount anymore.
2327 if (this.currentVolumeInfo_)
2328 this.closeOnUnmount_ = false;
2331 // Remember the current volume info.
2332 this.currentVolumeInfo_ = newCurrentVolumeInfo;
2334 this.selectionHandler_.onFileSelectionChanged();
2335 this.ui_.searchBox.clear();
2336 // TODO(mtomasz): Consider remembering the selection.
2337 util.updateAppState(
2338 this.getCurrentDirectoryEntry() ?
2339 this.getCurrentDirectoryEntry().toURL() : '',
2340 '' /* selectionURL */,
2341 '' /* opt_param */);
2343 if (this.commandHandler)
2344 this.commandHandler.updateAvailability();
2346 this.updateUnformattedVolumeStatus_();
2347 this.updateTitle_();
2349 var currentEntry = this.getCurrentDirectoryEntry();
2350 this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ?
2351 null : currentEntry;
2354 FileManager.prototype.updateUnformattedVolumeStatus_ = function() {
2355 var volumeInfo = this.volumeManager_.getVolumeInfo(
2356 this.directoryModel_.getCurrentDirEntry());
2358 if (volumeInfo && volumeInfo.error) {
2359 this.dialogDom_.setAttribute('unformatted', '');
2361 var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2362 if (volumeInfo.error == util.VolumeError.UNSUPPORTED_FILESYSTEM) {
2363 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2365 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2368 // Update 'canExecute' for format command so the format button's disabled
2369 // property is properly set.
2370 if (this.commandHandler)
2371 this.commandHandler.updateAvailability();
2373 this.dialogDom_.removeAttribute('unformatted');
2377 FileManager.prototype.findListItemForEvent_ = function(event) {
2378 return this.findListItemForNode_(event.touchedElement || event.srcElement);
2381 FileManager.prototype.findListItemForNode_ = function(node) {
2382 var item = this.currentList_.getListItemAncestor(node);
2383 // TODO(serya): list should check that.
2384 return item && this.currentList_.isItem(item) ? item : null;
2388 * Unload handler for the page. May be called manually for the file picker
2389 * dialog, because it closes by calling extension API functions that do not
2392 * TODO(hirono): This method is not called when Files.app is opend as a dialog
2393 * and is closed by the close button in the dialog frame. crbug.com/309967
2396 FileManager.prototype.onUnload_ = function() {
2397 if (this.directoryModel_)
2398 this.directoryModel_.dispose();
2399 if (this.volumeManager_)
2400 this.volumeManager_.dispose();
2401 if (this.filePopup_ &&
2402 this.filePopup_.contentWindow &&
2403 this.filePopup_.contentWindow.unload)
2404 this.filePopup_.contentWindow.unload(true /* exiting */);
2405 if (this.progressCenterPanel_)
2406 this.backgroundPage_.background.progressCenter.removePanel(
2407 this.progressCenterPanel_);
2408 if (this.fileOperationManager_) {
2409 if (this.onCopyProgressBound_) {
2410 this.fileOperationManager_.removeEventListener(
2411 'copy-progress', this.onCopyProgressBound_);
2413 if (this.onEntryChangedBound_) {
2414 this.fileOperationManager_.removeEventListener(
2415 'entry-changed', this.onEntryChangedBound_);
2418 window.closing = true;
2419 if (this.backgroundPage_ && util.platform.runningInBrowser())
2420 this.backgroundPage_.background.tryClose();
2423 FileManager.prototype.initiateRename = function() {
2424 var item = this.currentList_.ensureLeadItemExists();
2427 var label = item.querySelector('.filename-label');
2428 var input = this.renameInput_;
2430 input.value = label.textContent;
2431 label.parentNode.setAttribute('renaming', '');
2432 label.parentNode.appendChild(input);
2434 var selectionEnd = input.value.lastIndexOf('.');
2435 if (selectionEnd == -1) {
2438 input.selectionStart = 0;
2439 input.selectionEnd = selectionEnd;
2442 // This has to be set late in the process so we don't handle spurious
2444 input.currentEntry = this.currentList_.dataModel.item(item.listIndex);
2448 * @type {Event} Key event.
2451 FileManager.prototype.onRenameInputKeyDown_ = function(event) {
2452 if (!this.isRenamingInProgress())
2455 // Do not move selection or lead item in list during rename.
2456 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
2457 event.stopPropagation();
2460 switch (util.getKeyModifiers(event) + event.keyCode) {
2461 case '27': // Escape
2462 this.cancelRename_();
2463 event.preventDefault();
2467 this.commitRename_();
2468 event.preventDefault();
2474 * @type {Event} Blur event.
2477 FileManager.prototype.onRenameInputBlur_ = function(event) {
2478 if (this.isRenamingInProgress() && !this.renameInput_.validation_)
2479 this.commitRename_();
2485 FileManager.prototype.commitRename_ = function() {
2486 var input = this.renameInput_;
2487 var entry = input.currentEntry;
2488 var newName = input.value;
2490 if (newName == entry.name) {
2491 this.cancelRename_();
2495 var nameNode = this.findListItemForNode_(this.renameInput_).
2496 querySelector('.filename-label');
2498 input.validation_ = true;
2499 var validationDone = function(valid) {
2500 input.validation_ = false;
2501 // Alert dialog restores focus unless the item removed from DOM.
2502 if (this.document_.activeElement != input)
2503 this.cancelRename_();
2507 // Validation succeeded. Do renaming.
2509 this.cancelRename_();
2510 // Optimistically apply new name immediately to avoid flickering in
2512 nameNode.textContent = newName;
2516 function(newEntry) {
2517 this.directoryModel_.onRenameEntry(entry, newEntry);
2520 // Write back to the old name.
2521 nameNode.textContent = entry.name;
2523 // Show error dialog.
2525 if (error.name == util.FileError.PATH_EXISTS_ERR ||
2526 error.name == util.FileError.TYPE_MISMATCH_ERR) {
2527 // Check the existing entry is file or not.
2528 // 1) If the entry is a file:
2529 // a) If we get PATH_EXISTS_ERR, a file exists.
2530 // b) If we get TYPE_MISMATCH_ERR, a directory exists.
2531 // 2) If the entry is a directory:
2532 // a) If we get PATH_EXISTS_ERR, a directory exists.
2533 // b) If we get TYPE_MISMATCH_ERR, a file exists.
2535 (entry.isFile && error.name ==
2536 util.FileError.PATH_EXISTS_ERR) ||
2537 (!entry.isFile && error.name ==
2538 util.FileError.TYPE_MISMATCH_ERR) ?
2539 'FILE_ALREADY_EXISTS' :
2540 'DIRECTORY_ALREADY_EXISTS',
2543 message = strf('ERROR_RENAMING', entry.name,
2544 util.getFileErrorString(error.name));
2547 this.alert.show(message);
2551 // TODO(haruki): this.getCurrentDirectoryEntry() might not return the actual
2552 // parent if the directory content is a search result. Fix it to do proper
2554 this.validateFileName_(this.getCurrentDirectoryEntry(),
2556 validationDone.bind(this));
2562 FileManager.prototype.cancelRename_ = function() {
2563 this.renameInput_.currentEntry = null;
2565 var parent = this.renameInput_.parentNode;
2567 parent.removeAttribute('renaming');
2568 parent.removeChild(this.renameInput_);
2573 * @param {Event} Key event.
2576 FileManager.prototype.onFilenameInputInput_ = function() {
2577 this.selectionHandler_.updateOkButton();
2581 * @param {Event} Key event.
2584 FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
2585 if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */)
2586 this.okButton_.click();
2590 * @param {Event} Focus event.
2593 FileManager.prototype.onFilenameInputFocus_ = function(event) {
2594 var input = this.filenameInput_;
2596 // On focus we want to select everything but the extension, but
2597 // Chrome will select-all after the focus event completes. We
2598 // schedule a timeout to alter the focus after that happens.
2599 setTimeout(function() {
2600 var selectionEnd = input.value.lastIndexOf('.');
2601 if (selectionEnd == -1) {
2604 input.selectionStart = 0;
2605 input.selectionEnd = selectionEnd;
2613 FileManager.prototype.onScanStarted_ = function() {
2614 if (this.scanInProgress_) {
2615 this.table_.list.endBatchUpdates();
2616 this.grid_.endBatchUpdates();
2619 if (this.commandHandler)
2620 this.commandHandler.updateAvailability();
2621 this.table_.list.startBatchUpdates();
2622 this.grid_.startBatchUpdates();
2623 this.scanInProgress_ = true;
2625 this.scanUpdatedAtLeastOnceOrCompleted_ = false;
2626 if (this.scanCompletedTimer_) {
2627 clearTimeout(this.scanCompletedTimer_);
2628 this.scanCompletedTimer_ = null;
2631 if (this.scanUpdatedTimer_) {
2632 clearTimeout(this.scanUpdatedTimer_);
2633 this.scanUpdatedTimer_ = null;
2636 if (this.spinner_.hidden) {
2637 this.cancelSpinnerTimeout_();
2638 this.showSpinnerTimeout_ =
2639 setTimeout(this.showSpinner_.bind(this, true), 500);
2646 FileManager.prototype.onScanCompleted_ = function() {
2647 if (!this.scanInProgress_) {
2648 console.error('Scan-completed event recieved. But scan is not started.');
2652 if (this.commandHandler)
2653 this.commandHandler.updateAvailability();
2654 this.hideSpinnerLater_();
2656 if (this.scanUpdatedTimer_) {
2657 clearTimeout(this.scanUpdatedTimer_);
2658 this.scanUpdatedTimer_ = null;
2661 // To avoid flickering postpone updating the ui by a small amount of time.
2662 // There is a high chance, that metadata will be received within 50 ms.
2663 this.scanCompletedTimer_ = setTimeout(function() {
2664 // Check if batch updates are already finished by onScanUpdated_().
2665 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2666 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2667 this.updateMiddleBarVisibility_();
2670 this.scanInProgress_ = false;
2671 this.table_.list.endBatchUpdates();
2672 this.grid_.endBatchUpdates();
2673 this.scanCompletedTimer_ = null;
2680 FileManager.prototype.onScanUpdated_ = function() {
2681 if (!this.scanInProgress_) {
2682 console.error('Scan-updated event recieved. But scan is not started.');
2686 if (this.scanUpdatedTimer_ || this.scanCompletedTimer_)
2689 // Show contents incrementally by finishing batch updated, but only after
2690 // 200ms elapsed, to avoid flickering when it is not necessary.
2691 this.scanUpdatedTimer_ = setTimeout(function() {
2692 // We need to hide the spinner only once.
2693 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2694 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2695 this.hideSpinnerLater_();
2696 this.updateMiddleBarVisibility_();
2700 if (this.scanInProgress_) {
2701 this.table_.list.endBatchUpdates();
2702 this.grid_.endBatchUpdates();
2703 this.table_.list.startBatchUpdates();
2704 this.grid_.startBatchUpdates();
2706 this.scanUpdatedTimer_ = null;
2713 FileManager.prototype.onScanCancelled_ = function() {
2714 if (!this.scanInProgress_) {
2715 console.error('Scan-cancelled event recieved. But scan is not started.');
2719 if (this.commandHandler)
2720 this.commandHandler.updateAvailability();
2721 this.hideSpinnerLater_();
2722 if (this.scanCompletedTimer_) {
2723 clearTimeout(this.scanCompletedTimer_);
2724 this.scanCompletedTimer_ = null;
2726 if (this.scanUpdatedTimer_) {
2727 clearTimeout(this.scanUpdatedTimer_);
2728 this.scanUpdatedTimer_ = null;
2730 // Finish unfinished batch updates.
2731 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2732 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2733 this.updateMiddleBarVisibility_();
2736 this.scanInProgress_ = false;
2737 this.table_.list.endBatchUpdates();
2738 this.grid_.endBatchUpdates();
2742 * Handle the 'rescan-completed' from the DirectoryModel.
2745 FileManager.prototype.onRescanCompleted_ = function() {
2746 this.selectionHandler_.onFileSelectionChanged();
2752 FileManager.prototype.cancelSpinnerTimeout_ = function() {
2753 if (this.showSpinnerTimeout_) {
2754 clearTimeout(this.showSpinnerTimeout_);
2755 this.showSpinnerTimeout_ = null;
2762 FileManager.prototype.hideSpinnerLater_ = function() {
2763 this.cancelSpinnerTimeout_();
2764 this.showSpinner_(false);
2768 * @param {boolean} on True to show, false to hide.
2771 FileManager.prototype.showSpinner_ = function(on) {
2772 if (on && this.directoryModel_ && this.directoryModel_.isScanning())
2773 this.spinner_.hidden = false;
2775 if (!on && (!this.directoryModel_ ||
2776 !this.directoryModel_.isScanning() ||
2777 this.directoryModel_.getFileList().length != 0)) {
2778 this.spinner_.hidden = true;
2782 FileManager.prototype.createNewFolder = function() {
2783 var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
2785 // Find a name that doesn't exist in the data model.
2786 var files = this.directoryModel_.getFileList();
2788 for (var i = 0; i < files.length; i++) {
2789 var name = files.item(i).name;
2790 // Filtering names prevents from conflicts with prototype's names
2792 if (name.substring(0, defaultName.length) == defaultName)
2796 var baseName = defaultName;
2801 var advance = function() {
2807 var current = function() {
2808 return baseName + separator + index + suffix;
2811 // Accessing hasOwnProperty is safe since hash properties filtered.
2812 while (hash.hasOwnProperty(current())) {
2817 var list = self.currentList_;
2818 var tryCreate = function() {
2819 self.directoryModel_.createDirectory(current(),
2820 onSuccess, onError);
2823 var onSuccess = function(entry) {
2824 metrics.recordUserAction('CreateNewFolder');
2825 list.selectedItem = entry;
2826 self.initiateRename();
2829 var onError = function(error) {
2830 self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
2831 util.getFileErrorString(error.name)));
2838 * @param {Event} event Click event.
2841 FileManager.prototype.onDetailViewButtonClick_ = function(event) {
2842 // Stop propagate and hide the menu manually, in order to prevent the focus
2843 // from being back to the button. (cf. http://crbug.com/248479)
2844 event.stopPropagation();
2845 this.gearButton_.hideMenu();
2846 this.gearButton_.blur();
2847 this.setListType(FileManager.ListType.DETAIL);
2851 * @param {Event} event Click event.
2854 FileManager.prototype.onThumbnailViewButtonClick_ = function(event) {
2855 // Stop propagate and hide the menu manually, in order to prevent the focus
2856 // from being back to the button. (cf. http://crbug.com/248479)
2857 event.stopPropagation();
2858 this.gearButton_.hideMenu();
2859 this.gearButton_.blur();
2860 this.setListType(FileManager.ListType.THUMBNAIL);
2864 * KeyDown event handler for the document.
2865 * @param {Event} event Key event.
2868 FileManager.prototype.onKeyDown_ = function(event) {
2869 if (event.srcElement === this.renameInput_) {
2870 // Ignore keydown handler in the rename input box.
2874 switch (util.getKeyModifiers(event) + event.keyCode) {
2875 case 'Ctrl-190': // Ctrl-. => Toggle filter files.
2876 this.fileFilter_.setFilterHidden(
2877 !this.fileFilter_.isFilterHiddenOn());
2878 event.preventDefault();
2881 case '27': // Escape => Cancel dialog.
2882 if (this.dialogType != DialogType.FULL_PAGE) {
2883 // If there is nothing else for ESC to do, then cancel the dialog.
2884 event.preventDefault();
2885 this.cancelButton_.click();
2892 * KeyDown event handler for the div#list-container element.
2893 * @param {Event} event Key event.
2896 FileManager.prototype.onListKeyDown_ = function(event) {
2897 if (event.srcElement.tagName == 'INPUT') {
2898 // Ignore keydown handler in the rename input box.
2902 switch (util.getKeyModifiers(event) + event.keyCode) {
2903 case '8': // Backspace => Up one directory.
2904 event.preventDefault();
2905 // TODO(mtomasz): Use Entry.getParent() instead.
2906 if (!this.getCurrentDirectoryEntry())
2908 var currentEntry = this.getCurrentDirectoryEntry();
2909 var locationInfo = this.volumeManager_.getLocationInfo(currentEntry);
2910 // TODO(mtomasz): There may be a tiny race in here.
2911 if (locationInfo && !locationInfo.isRootEntry &&
2912 !locationInfo.isSpecialSearchRoot) {
2913 currentEntry.getParent(function(parentEntry) {
2914 this.directoryModel_.changeDirectoryEntry(parentEntry);
2915 }.bind(this), function() { /* Ignore errors. */});
2919 case '13': // Enter => Change directory or perform default action.
2920 // TODO(dgozman): move directory action to dispatchSelectionAction.
2921 var selection = this.getSelection();
2922 if (selection.totalCount == 1 &&
2923 selection.entries[0].isDirectory &&
2924 !DialogType.isFolderDialog(this.dialogType)) {
2925 event.preventDefault();
2926 this.onDirectoryAction_(selection.entries[0]);
2927 } else if (this.dispatchSelectionAction_()) {
2928 event.preventDefault();
2933 switch (event.keyIdentifier) {
2940 // When navigating with keyboard we hide the distracting mouse hover
2941 // highlighting until the user moves the mouse again.
2942 this.setNoHover_(true);
2948 * Suppress/restore hover highlighting in the list container.
2949 * @param {boolean} on True to temporarity hide hover state.
2952 FileManager.prototype.setNoHover_ = function(on) {
2954 this.listContainer_.classList.add('nohover');
2956 this.listContainer_.classList.remove('nohover');
2961 * KeyPress event handler for the div#list-container element.
2962 * @param {Event} event Key event.
2965 FileManager.prototype.onListKeyPress_ = function(event) {
2966 if (event.srcElement.tagName == 'INPUT') {
2967 // Ignore keypress handler in the rename input box.
2971 if (event.ctrlKey || event.metaKey || event.altKey)
2974 var now = new Date();
2975 var char = String.fromCharCode(event.charCode).toLowerCase();
2976 var text = now - this.textSearchState_.date > 1000 ? '' :
2977 this.textSearchState_.text;
2978 this.textSearchState_ = {text: text + char, date: now};
2980 this.doTextSearch_();
2984 * Mousemove event handler for the div#list-container element.
2985 * @param {Event} event Mouse event.
2988 FileManager.prototype.onListMouseMove_ = function(event) {
2989 // The user grabbed the mouse, restore the hover highlighting.
2990 this.setNoHover_(false);
2994 * Performs a 'text search' - selects a first list entry with name
2995 * starting with entered text (case-insensitive).
2998 FileManager.prototype.doTextSearch_ = function() {
2999 var text = this.textSearchState_.text;
3003 var dm = this.directoryModel_.getFileList();
3004 for (var index = 0; index < dm.length; ++index) {
3005 var name = dm.item(index).name;
3006 if (name.substring(0, text.length).toLowerCase() == text) {
3007 this.currentList_.selectionModel.selectedIndexes = [index];
3012 this.textSearchState_.text = '';
3016 * Handle a click of the cancel button. Closes the window.
3017 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3019 * @param {Event} event The click event.
3022 FileManager.prototype.onCancel_ = function(event) {
3023 chrome.fileBrowserPrivate.cancelDialog();
3029 * Resolves selected file urls returned from an Open dialog.
3031 * For drive files this involves some special treatment.
3032 * Starts getting drive files if needed.
3034 * @param {Array.<string>} fileUrls Drive URLs.
3035 * @param {function(Array.<string>)} callback To be called with fixed URLs.
3038 FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) {
3039 if (this.isOnDrive()) {
3040 chrome.fileBrowserPrivate.getDriveFiles(
3042 function(localPaths) {
3051 * Closes this modal dialog with some files selected.
3052 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3053 * @param {Object} selection Contains urls, filterIndex and multiple fields.
3056 FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) {
3058 function callback() {
3062 if (selection.multiple) {
3063 chrome.fileBrowserPrivate.selectFiles(
3064 selection.urls, this.params_.shouldReturnLocalPath, callback);
3066 var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE);
3067 chrome.fileBrowserPrivate.selectFile(
3068 selection.urls[0], selection.filterIndex, forOpening,
3069 this.params_.shouldReturnLocalPath, callback);
3074 * Tries to close this modal dialog with some files selected.
3075 * Performs preprocessing if needed (e.g. for Drive).
3076 * @param {Object} selection Contains urls, filterIndex and multiple fields.
3079 FileManager.prototype.selectFilesAndClose_ = function(selection) {
3080 if (!this.isOnDrive() ||
3081 this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3082 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3086 var shade = this.document_.createElement('div');
3087 shade.className = 'shade';
3088 var footer = this.dialogDom_.querySelector('.button-panel');
3089 var progress = footer.querySelector('.progress-track');
3090 progress.style.width = '0%';
3091 var cancelled = false;
3093 var progressMap = {};
3094 var filesStarted = 0;
3095 var filesTotal = selection.urls.length;
3096 for (var index = 0; index < selection.urls.length; index++) {
3097 progressMap[selection.urls[index]] = -1;
3099 var lastPercent = 0;
3103 var onFileTransfersUpdated = function(statusList) {
3104 for (var index = 0; index < statusList.length; index++) {
3105 var status = statusList[index];
3106 var escaped = encodeURI(status.fileUrl);
3107 if (!(escaped in progressMap)) continue;
3108 if (status.total == -1) continue;
3110 var old = progressMap[escaped];
3112 // -1 means we don't know file size yet.
3113 bytesTotal += status.total;
3117 bytesDone += status.processed - old;
3118 progressMap[escaped] = status.processed;
3121 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
3122 // For files we don't have information about, assume the progress is zero.
3123 percent = percent * filesStarted / filesTotal * 100;
3124 // Do not decrease the progress. This may happen, if first downloaded
3125 // file is small, and the second one is large.
3126 lastPercent = Math.max(lastPercent, percent);
3127 progress.style.width = lastPercent + '%';
3130 var setup = function() {
3131 this.document_.querySelector('.dialog-container').appendChild(shade);
3132 setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100);
3133 footer.setAttribute('progress', 'progress');
3134 this.cancelButton_.removeEventListener('click', this.onCancelBound_);
3135 this.cancelButton_.addEventListener('click', onCancel);
3136 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
3137 onFileTransfersUpdated);
3140 var cleanup = function() {
3141 shade.parentNode.removeChild(shade);
3142 footer.removeAttribute('progress');
3143 this.cancelButton_.removeEventListener('click', onCancel);
3144 this.cancelButton_.addEventListener('click', this.onCancelBound_);
3145 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
3146 onFileTransfersUpdated);
3149 var onCancel = function() {
3151 // According to API cancel may fail, but there is no proper UI to reflect
3152 // this. So, we just silently assume that everything is cancelled.
3153 chrome.fileBrowserPrivate.cancelFileTransfers(
3154 selection.urls, function(response) {});
3158 var onResolved = function(resolvedUrls) {
3159 if (cancelled) return;
3161 selection.urls = resolvedUrls;
3162 // Call next method on a timeout, as it's unsafe to
3163 // close a window from a callback.
3164 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3167 var onProperties = function(properties) {
3168 for (var i = 0; i < properties.length; i++) {
3169 if (!properties[i] || properties[i].present) {
3170 // For files already in GCache, we don't get any transfer updates.
3174 this.resolveSelectResults_(selection.urls, onResolved);
3179 // TODO(mtomasz): Use Entry instead of URLs, if possible.
3180 util.URLsToEntries(selection.urls, function(entries) {
3181 this.metadataCache_.get(entries, 'drive', onProperties);
3186 * Handle a click of the ok button.
3188 * The ok button has different UI labels depending on the type of dialog, but
3189 * in code it's always referred to as 'ok'.
3191 * @param {Event} event The click event.
3194 FileManager.prototype.onOk_ = function(event) {
3195 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3196 // Save-as doesn't require a valid selection from the list, since
3197 // we're going to take the filename from the text input.
3198 var filename = this.filenameInput_.value;
3200 throw new Error('Missing filename!');
3202 var directory = this.getCurrentDirectoryEntry();
3203 this.validateFileName_(directory, filename, function(isValid) {
3207 if (util.isFakeEntry(directory)) {
3208 // Can't save a file into a fake directory.
3212 var selectFileAndClose = function() {
3213 // TODO(mtomasz): Clean this up by avoiding constructing a URL
3214 // via string concatenation.
3215 var currentDirUrl = directory.toURL();
3216 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
3217 currentDirUrl += '/';
3218 this.selectFilesAndClose_({
3219 urls: [currentDirUrl + encodeURIComponent(filename)],
3221 filterIndex: this.getSelectedFilterIndex_(filename)
3226 filename, {create: false},
3228 // An existing file is found. Show confirmation dialog to
3229 // overwrite it. If the user select "OK" on the dialog, save it.
3230 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
3231 selectFileAndClose);
3234 if (error.name == util.FileError.NOT_FOUND_ERR) {
3235 // The file does not exist, so it should be ok to create a
3237 selectFileAndClose();
3240 if (error.name == util.FileError.TYPE_MISMATCH_ERR) {
3241 // An directory is found.
3242 // Do not allow to overwrite directory.
3243 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
3247 // Unexpected error.
3248 console.error('File save failed: ' + error.code);
3255 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
3257 if (DialogType.isFolderDialog(this.dialogType) &&
3258 selectedIndexes.length == 0) {
3259 var url = this.getCurrentDirectoryEntry().toURL();
3260 var singleSelection = {
3263 filterIndex: this.getSelectedFilterIndex_()
3265 this.selectFilesAndClose_(singleSelection);
3269 // All other dialog types require at least one selected list item.
3270 // The logic to control whether or not the ok button is enabled should
3271 // prevent us from ever getting here, but we sanity check to be sure.
3272 if (!selectedIndexes.length)
3273 throw new Error('Nothing selected!');
3275 var dm = this.directoryModel_.getFileList();
3276 for (var i = 0; i < selectedIndexes.length; i++) {
3277 var entry = dm.item(selectedIndexes[i]);
3279 console.error('Error locating selected file at index: ' + i);
3283 files.push(entry.toURL());
3286 // Multi-file selection has no other restrictions.
3287 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
3288 var multipleSelection = {
3292 this.selectFilesAndClose_(multipleSelection);
3296 // Everything else must have exactly one.
3297 if (files.length > 1)
3298 throw new Error('Too many files selected!');
3300 var selectedEntry = dm.item(selectedIndexes[0]);
3302 if (DialogType.isFolderDialog(this.dialogType)) {
3303 if (!selectedEntry.isDirectory)
3304 throw new Error('Selected entry is not a folder!');
3305 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
3306 if (!selectedEntry.isFile)
3307 throw new Error('Selected entry is not a file!');
3310 var singleSelection = {
3313 filterIndex: this.getSelectedFilterIndex_()
3315 this.selectFilesAndClose_(singleSelection);
3319 * Verifies the user entered name for file or folder to be created or
3320 * renamed to. Name restrictions must correspond to File API restrictions
3321 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is
3322 * out of date (spec is
3323 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to
3324 * be fixed. Shows message box if the name is invalid.
3326 * It also verifies if the name length is in the limit of the filesystem.
3328 * @param {DirectoryEntry} parentEntry The URL of the parent directory entry.
3329 * @param {string} name New file or folder name.
3330 * @param {function} onDone Function to invoke when user closes the
3331 * warning box or immediatelly if file name is correct. If the name was
3332 * valid it is passed true, and false otherwise.
3335 FileManager.prototype.validateFileName_ = function(
3336 parentEntry, name, onDone) {
3338 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
3340 msg = strf('ERROR_INVALID_CHARACTER', testResult[0]);
3341 } else if (/^\s*$/i.test(name)) {
3342 msg = str('ERROR_WHITESPACE_NAME');
3343 } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) {
3344 msg = str('ERROR_RESERVED_NAME');
3345 } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') {
3346 msg = str('ERROR_HIDDEN_NAME');
3350 this.alert.show(msg, function() {
3357 chrome.fileBrowserPrivate.validatePathNameLength(
3358 parentEntry.toURL(), name, function(valid) {
3360 self.alert.show(str('ERROR_LONG_NAME'),
3361 function() { onDone(false); });
3369 * Handler invoked on preference setting in drive context menu.
3371 * @param {string} pref The preference to alter.
3372 * @param {boolean} inverted Invert the value if true.
3373 * @param {Event} event The click event.
3376 FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) {
3377 var newValue = !event.target.hasAttribute('checked');
3379 event.target.setAttribute('checked', 'checked');
3381 event.target.removeAttribute('checked');
3383 var changeInfo = {};
3384 changeInfo[pref] = inverted ? !newValue : newValue;
3385 chrome.fileBrowserPrivate.setPreferences(changeInfo);
3389 * Invoked when the search box is changed.
3391 * @param {Event} event The changed event.
3394 FileManager.prototype.onSearchBoxUpdate_ = function(event) {
3395 var searchString = this.searchBox_.value;
3397 if (this.isOnDrive()) {
3398 // When the search text is changed, finishes the search and showes back
3399 // the last directory by passing an empty string to
3400 // {@code DirectoryModel.search()}.
3401 if (this.directoryModel_.isSearching() &&
3402 this.lastSearchQuery_ != searchString) {
3406 // On drive, incremental search is not invoked since we have an auto-
3407 // complete suggestion instead.
3411 this.search_(searchString);
3415 * Handle the search clear button click.
3418 FileManager.prototype.onSearchClearButtonClick_ = function() {
3419 this.ui_.searchBox.clear();
3420 this.onSearchBoxUpdate_();
3424 * Search files and update the list with the search result.
3426 * @param {string} searchString String to be searched with.
3429 FileManager.prototype.search_ = function(searchString) {
3430 var noResultsDiv = this.document_.getElementById('no-search-results');
3432 var reportEmptySearchResults = function() {
3433 if (this.directoryModel_.getFileList().length === 0) {
3434 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
3435 // hence we escapes |searchString| here.
3436 var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
3437 util.htmlEscape(searchString));
3438 noResultsDiv.innerHTML = html;
3439 noResultsDiv.setAttribute('show', 'true');
3441 noResultsDiv.removeAttribute('show');
3445 var hideNoResultsDiv = function() {
3446 noResultsDiv.removeAttribute('show');
3449 this.doSearch(searchString,
3450 reportEmptySearchResults.bind(this),
3451 hideNoResultsDiv.bind(this));
3455 * Performs search and displays results.
3457 * @param {string} query Query that will be searched for.
3458 * @param {function()=} opt_onSearchRescan Function that will be called when
3459 * the search directory is rescanned (i.e. search results are displayed).
3460 * @param {function()=} opt_onClearSearch Function to be called when search
3461 * state gets cleared.
3463 FileManager.prototype.doSearch = function(
3464 searchString, opt_onSearchRescan, opt_onClearSearch) {
3465 var onSearchRescan = opt_onSearchRescan || function() {};
3466 var onClearSearch = opt_onClearSearch || function() {};
3468 this.lastSearchQuery_ = searchString;
3469 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
3473 * Requests autocomplete suggestions for files on Drive.
3474 * Once the suggestions are returned, the autocomplete popup will show up.
3476 * @param {string} query The text to autocomplete from.
3479 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
3480 query = query.trimLeft();
3482 // Only Drive supports auto-compelete
3483 if (!this.isOnDrive())
3486 // Remember the most recent query. If there is an other request in progress,
3487 // then it's result will be discarded and it will call a new request for
3489 this.lastAutocompleteQuery_ = query;
3490 if (this.autocompleteSuggestionsBusy_)
3493 // The autocomplete list should be resized and repositioned here as the
3494 // search box is resized when it's focused.
3495 this.autocompleteList_.syncWidthAndPositionToInput();
3498 this.autocompleteList_.suggestions = [];
3502 var headerItem = {isHeaderItem: true, searchQuery: query};
3503 if (!this.autocompleteList_.dataModel ||
3504 this.autocompleteList_.dataModel.length == 0)
3505 this.autocompleteList_.suggestions = [headerItem];
3507 // Updates only the head item to prevent a flickering on typing.
3508 this.autocompleteList_.dataModel.splice(0, 1, headerItem);
3510 this.autocompleteSuggestionsBusy_ = true;
3512 var searchParams = {
3517 chrome.fileBrowserPrivate.searchDriveMetadata(
3519 function(suggestions) {
3520 this.autocompleteSuggestionsBusy_ = false;
3522 // Discard results for previous requests and fire a new search
3523 // for the most recent query.
3524 if (query != this.lastAutocompleteQuery_) {
3525 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
3529 // Keeps the items in the suggestion list.
3530 this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
3535 * Opens the currently selected suggestion item.
3538 FileManager.prototype.openAutocompleteSuggestion_ = function() {
3539 var selectedItem = this.autocompleteList_.selectedItem;
3541 // If the entry is the search item or no entry is selected, just change to
3542 // the search result.
3543 if (!selectedItem || selectedItem.isHeaderItem) {
3544 var query = selectedItem ?
3545 selectedItem.searchQuery : this.searchBox_.value;
3546 this.search_(query);
3550 var entry = selectedItem.entry;
3551 // If the entry is a directory, just change the directory.
3552 if (entry.isDirectory) {
3553 this.onDirectoryAction_(entry);
3557 var entries = [entry];
3560 // To open a file, first get the mime type.
3561 this.metadataCache_.get(entries, 'drive', function(props) {
3562 var mimeType = props[0].contentMimeType || '';
3563 var mimeTypes = [mimeType];
3564 var openIt = function() {
3565 if (self.dialogType == DialogType.FULL_PAGE) {
3566 var tasks = new FileTasks(self);
3567 tasks.init(entries, mimeTypes);
3568 tasks.executeDefault();
3574 // Change the current directory to the directory that contains the
3575 // selected file. Note that this is necessary for an image or a video,
3576 // which should be opened in the gallery mode, as the gallery mode
3577 // requires the entry to be in the current directory model. For
3578 // consistency, the current directory is always changed regardless of
3580 entry.getParent(function(parentEntry) {
3581 var onDirectoryChanged = function(event) {
3582 self.directoryModel_.removeEventListener('scan-completed',
3583 onDirectoryChanged);
3584 self.directoryModel_.selectEntry(entry);
3587 // changeDirectoryEntry() returns immediately. We should wait until the
3588 // directory scan is complete.
3589 self.directoryModel_.addEventListener('scan-completed',
3590 onDirectoryChanged);
3591 self.directoryModel_.changeDirectoryEntry(
3594 // Remove the listner if the change directory failed.
3595 self.directoryModel_.removeEventListener('scan-completed',
3596 onDirectoryChanged);
3602 FileManager.prototype.decorateSplitter = function(splitterElement) {
3605 var Splitter = cr.ui.Splitter;
3607 var customSplitter = cr.ui.define('div');
3609 customSplitter.prototype = {
3610 __proto__: Splitter.prototype,
3612 handleSplitterDragStart: function(e) {
3613 Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
3614 this.ownerDocument.documentElement.classList.add('col-resize');
3617 handleSplitterDragMove: function(deltaX) {
3618 Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
3622 handleSplitterDragEnd: function(e) {
3623 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
3624 this.ownerDocument.documentElement.classList.remove('col-resize');
3628 customSplitter.decorate(splitterElement);
3632 * Updates default action menu item to match passed taskItem (icon,
3633 * label and action).
3635 * @param {Object} defaultItem - taskItem to match.
3636 * @param {boolean} isMultiple - if multiple tasks available.
3638 FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
3641 if (defaultItem.iconType) {
3642 this.defaultActionMenuItem_.style.backgroundImage = '';
3643 this.defaultActionMenuItem_.setAttribute('file-type-icon',
3644 defaultItem.iconType);
3645 } else if (defaultItem.iconUrl) {
3646 this.defaultActionMenuItem_.style.backgroundImage =
3647 'url(' + defaultItem.iconUrl + ')';
3649 this.defaultActionMenuItem_.style.backgroundImage = '';
3652 this.defaultActionMenuItem_.label = defaultItem.title;
3653 this.defaultActionMenuItem_.disabled = !!defaultItem.disabled;
3654 this.defaultActionMenuItem_.taskId = defaultItem.taskId;
3657 var defaultActionSeparator =
3658 this.dialogDom_.querySelector('#default-action-separator');
3660 this.openWithCommand_.canExecuteChange();
3661 this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
3662 this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled;
3664 this.defaultActionMenuItem_.hidden = !defaultItem;
3665 defaultActionSeparator.hidden = !defaultItem;
3669 * Window beforeunload handler.
3670 * @return {string} Message to show. Ignored when running as a packaged app.
3673 FileManager.prototype.onBeforeUnload_ = function() {
3674 if (this.filePopup_ &&
3675 this.filePopup_.contentWindow &&
3676 this.filePopup_.contentWindow.beforeunload) {
3677 // The gallery might want to prevent the unload if it is busy.
3678 return this.filePopup_.contentWindow.beforeunload();
3684 * @return {FileSelection} Selection object.
3686 FileManager.prototype.getSelection = function() {
3687 return this.selectionHandler_.selection;
3691 * @return {ArrayDataModel} File list.
3693 FileManager.prototype.getFileList = function() {
3694 return this.directoryModel_.getFileList();
3698 * @return {cr.ui.List} Current list object.
3700 FileManager.prototype.getCurrentList = function() {
3701 return this.currentList_;
3705 * Retrieve the preferences of the files.app. This method caches the result
3706 * and returns it unless opt_update is true.
3707 * @param {function(Object.<string, *>)} callback Callback to get the
3709 * @param {boolean=} opt_update If is's true, don't use the cache and
3710 * retrieve latest preference. Default is false.
3713 FileManager.prototype.getPreferences_ = function(callback, opt_update) {
3714 if (!opt_update && this.preferences_ !== undefined) {
3715 callback(this.preferences_);
3719 chrome.fileBrowserPrivate.getPreferences(function(prefs) {
3720 this.preferences_ = prefs;