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 * True while a user is pressing <Tab>.
28 * This is used for identifying the trigger causing the filelist to
33 this.pressingTab_ = false;
36 * True while a user is pressing <Ctrl>.
38 * TODO(fukino): This key is used only for controlling gear menu, so it
39 * should be moved to GearMenu class. crbug.com/366032.
44 this.pressingCtrl_ = false;
47 * True if shown gear menu is in secret mode.
49 * TODO(fukino): The state of gear menu should be moved to GearMenu class.
55 this.isSecretGearMenuShown_ = false;
59 * @type {SelectionHandler}
62 this.selectionHandler_ = null;
65 * VolumeInfo of the current volume.
69 this.currentVolumeInfo_ = null;
72 FileManager.prototype = {
73 __proto__: cr.EventTarget.prototype,
74 get directoryModel() {
75 return this.directoryModel_;
78 return this.directoryTree_;
81 return this.document_;
83 get fileTransferController() {
84 return this.fileTransferController_;
86 get fileOperationManager() {
87 return this.fileOperationManager_;
89 get backgroundPage() {
90 return this.backgroundPage_;
93 return this.volumeManager_;
101 * List of dialog types.
103 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
104 * FULL_PAGE which is specific to this code.
110 SELECT_FOLDER: 'folder',
111 SELECT_UPLOAD_FOLDER: 'upload-folder',
112 SELECT_SAVEAS_FILE: 'saveas-file',
113 SELECT_OPEN_FILE: 'open-file',
114 SELECT_OPEN_MULTI_FILE: 'open-multi-file',
115 FULL_PAGE: 'full-page'
119 * @param {string} type Dialog type.
120 * @return {boolean} Whether the type is modal.
122 DialogType.isModal = function(type) {
123 return type == DialogType.SELECT_FOLDER ||
124 type == DialogType.SELECT_UPLOAD_FOLDER ||
125 type == DialogType.SELECT_SAVEAS_FILE ||
126 type == DialogType.SELECT_OPEN_FILE ||
127 type == DialogType.SELECT_OPEN_MULTI_FILE;
131 * @param {string} type Dialog type.
132 * @return {boolean} Whether the type is open dialog.
134 DialogType.isOpenDialog = function(type) {
135 return type == DialogType.SELECT_OPEN_FILE ||
136 type == DialogType.SELECT_OPEN_MULTI_FILE ||
137 type == DialogType.SELECT_FOLDER ||
138 type == DialogType.SELECT_UPLOAD_FOLDER;
142 * @param {string} type Dialog type.
143 * @return {boolean} Whether the type is open dialog for file(s).
145 DialogType.isOpenFileDialog = function(type) {
146 return type == DialogType.SELECT_OPEN_FILE ||
147 type == DialogType.SELECT_OPEN_MULTI_FILE;
151 * @param {string} type Dialog type.
152 * @return {boolean} Whether the type is folder selection dialog.
154 DialogType.isFolderDialog = function(type) {
155 return type == DialogType.SELECT_FOLDER ||
156 type == DialogType.SELECT_UPLOAD_FOLDER;
159 Object.freeze(DialogType);
162 * Bottom margin of the list and tree for transparent preview panel.
165 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
167 // Anonymous "namespace".
170 // Private variables and helper functions.
173 * Number of milliseconds in a day.
175 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
178 * Some UI elements react on a single click and standard double click handling
179 * leads to confusing results. We ignore a second click if it comes soon
182 var DOUBLE_CLICK_TIMEOUT = 200;
185 * Updates the element to display the information about remaining space for
188 * @param {!Object<string, number>} sizeStatsResult Map containing remaining
190 * @param {!Element} spaceInnerBar Block element for a percentage bar
191 * representing the remaining space.
192 * @param {!Element} spaceInfoLabel Inline element to contain the message.
193 * @param {!Element} spaceOuterBar Block element around the percentage bar.
195 var updateSpaceInfo = function(
196 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
197 spaceInnerBar.removeAttribute('pending');
198 if (sizeStatsResult) {
199 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
200 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
203 sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
204 spaceInnerBar.style.width =
205 (100 * usedSpace / sizeStatsResult.totalSize) + '%';
207 spaceOuterBar.hidden = false;
209 spaceOuterBar.hidden = true;
210 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
216 FileManager.ListType = {
221 FileManager.prototype.initPreferences_ = function(callback) {
222 var group = new AsyncUtil.Group();
224 // DRIVE preferences should be initialized before creating DirectoryModel
225 // to rebuild the roots list.
226 group.add(this.getPreferences_.bind(this));
228 // Get startup preferences.
229 this.viewOptions_ = {};
230 group.add(function(done) {
231 util.platform.getPreference(this.startupPrefName_, function(value) {
232 // Load the global default options.
234 this.viewOptions_ = JSON.parse(value);
236 // Override with window-specific options.
237 if (window.appState && window.appState.viewOptions) {
238 for (var key in window.appState.viewOptions) {
239 if (window.appState.viewOptions.hasOwnProperty(key))
240 this.viewOptions_[key] = window.appState.viewOptions[key];
251 * One time initialization for the file system and related things.
253 * @param {function()} callback Completion callback.
256 FileManager.prototype.initFileSystemUI_ = function(callback) {
257 this.table_.startBatchUpdates();
258 this.grid_.startBatchUpdates();
260 this.initFileList_();
261 this.setupCurrentDirectory_();
263 // PyAuto tests monitor this state by polling this variable
264 this.__defineGetter__('workerInitialized_', function() {
265 return this.metadataCache_.isInitialized();
268 this.initDateTimeFormatters_();
272 // Get the 'allowRedeemOffers' preference before launching
273 // FileListBannerController.
274 this.getPreferences_(function(pref) {
275 /** @type {boolean} */
276 var showOffers = pref['allowRedeemOffers'];
277 self.bannersController_ = new FileListBannerController(
278 self.directoryModel_, self.volumeManager_, self.document_,
280 self.bannersController_.addEventListener('relayout',
281 self.onResize_.bind(self));
284 var dm = this.directoryModel_;
285 dm.addEventListener('directory-changed',
286 this.onDirectoryChanged_.bind(this));
287 dm.addEventListener('begin-update-files', function() {
288 self.currentList_.startBatchUpdates();
290 dm.addEventListener('end-update-files', function() {
291 self.restoreItemBeingRenamed_();
292 self.currentList_.endBatchUpdates();
294 dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
295 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
296 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
297 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
298 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
299 dm.addEventListener('rescan-completed',
300 this.onRescanCompleted_.bind(this));
302 this.directoryTree_.addEventListener('change', function() {
303 this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
306 var stateChangeHandler =
307 this.onPreferencesChanged_.bind(this);
308 chrome.fileBrowserPrivate.onPreferencesChanged.addListener(
310 stateChangeHandler();
312 var driveConnectionChangedHandler =
313 this.onDriveConnectionChanged_.bind(this);
314 this.volumeManager_.addEventListener('drive-connection-changed',
315 driveConnectionChangedHandler);
316 driveConnectionChangedHandler();
318 // Set the initial focus.
320 // Set it as a fallback when there is no focus.
321 this.document_.addEventListener('focusout', function(e) {
322 setTimeout(function() {
323 // When there is no focus, the active element is the <body>.
324 if (this.document_.activeElement == this.document_.body)
329 this.initDataTransferOperations_();
331 this.initContextMenus_();
332 this.initCommands_();
334 this.updateFileTypeFilter_();
336 this.selectionHandler_.onFileSelectionChanged();
338 this.table_.endBatchUpdates();
339 this.grid_.endBatchUpdates();
345 * If |item| in the directory tree is behind the preview panel, scrolls up the
346 * parent view and make the item visible. This should be called when:
347 * - the selected item is changed in the directory tree.
348 * - the visibility of the the preview panel is changed.
352 FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ =
354 var selectedSubTree = this.directoryTree_.selectedItem;
355 if (!selectedSubTree)
357 var item = selectedSubTree.rowElement;
358 var parentView = this.directoryTree_;
360 var itemRect = item.getBoundingClientRect();
364 var listRect = parentView.getBoundingClientRect();
368 var previewPanel = this.dialogDom_.querySelector('.preview-panel');
369 var previewPanelRect = previewPanel.getBoundingClientRect();
370 var panelHeight = previewPanelRect ? previewPanelRect.height : 0;
372 var itemBottom = itemRect.bottom;
373 var listBottom = listRect.bottom - panelHeight;
375 if (itemBottom > listBottom) {
376 var scrollOffset = itemBottom - listBottom;
377 parentView.scrollTop += scrollOffset;
384 FileManager.prototype.initDateTimeFormatters_ = function() {
385 var use12hourClock = !this.preferences_['use24hourClock'];
386 this.table_.setDateTimeFormat(use12hourClock);
392 FileManager.prototype.initDataTransferOperations_ = function() {
393 this.fileOperationManager_ =
394 this.backgroundPage_.background.fileOperationManager;
396 // CopyManager are required for 'Delete' operation in
397 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
398 if (this.dialogType != DialogType.FULL_PAGE) return;
400 // TODO(hidehiko): Extract FileOperationManager related code from
401 // FileManager to simplify it.
402 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
403 this.fileOperationManager_.addEventListener(
404 'copy-progress', this.onCopyProgressBound_);
406 this.onEntriesChangedBound_ = this.onEntriesChanged_.bind(this);
407 this.fileOperationManager_.addEventListener(
408 'entries-changed', this.onEntriesChangedBound_);
410 var controller = this.fileTransferController_ =
411 new FileTransferController(this.document_,
412 this.fileOperationManager_,
414 this.directoryModel_,
416 this.ui_.multiProfileShareDialog);
417 controller.attachDragSource(this.table_.list);
418 controller.attachFileListDropTarget(this.table_.list);
419 controller.attachDragSource(this.grid_);
420 controller.attachFileListDropTarget(this.grid_);
421 controller.attachTreeDropTarget(this.directoryTree_);
422 controller.attachCopyPasteHandlers();
423 controller.addEventListener('selection-copied',
424 this.blinkSelection.bind(this));
425 controller.addEventListener('selection-cut',
426 this.blinkSelection.bind(this));
427 controller.addEventListener('source-not-found',
428 this.onSourceNotFound_.bind(this));
432 * Handles an error that the source entry of file operation is not found.
435 FileManager.prototype.onSourceNotFound_ = function(event) {
436 // Ensure this.sourceNotFoundErrorCount_ is integer.
437 this.sourceNotFoundErrorCount_ = ~~this.sourceNotFoundErrorCount_;
438 var item = new ProgressCenterItem();
439 item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_;
440 if (event.progressType === ProgressItemType.COPY)
441 item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', event.fileName);
442 else if (event.progressType === ProgressItemType.MOVE)
443 item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', event.fileName);
444 item.state = ProgressItemState.ERROR;
445 this.backgroundPage_.background.progressCenter.updateItem(item);
446 this.sourceNotFoundErrorCount_++;
450 * One-time initialization of context menus.
453 FileManager.prototype.initContextMenus_ = function() {
454 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
455 cr.ui.Menu.decorate(this.fileContextMenu_);
457 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
458 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
459 this.fileContextMenu_);
460 cr.ui.contextMenuHandler.setContextMenu(
461 this.document_.querySelector('.drive-welcome.page'),
462 this.fileContextMenu_);
464 this.rootsContextMenu_ =
465 this.dialogDom_.querySelector('#roots-context-menu');
466 cr.ui.Menu.decorate(this.rootsContextMenu_);
467 this.directoryTree_.contextMenuForRootItems = this.rootsContextMenu_;
469 this.directoryTreeContextMenu_ =
470 this.dialogDom_.querySelector('#directory-tree-context-menu');
471 cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
472 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
474 this.textContextMenu_ =
475 this.dialogDom_.querySelector('#text-context-menu');
476 cr.ui.Menu.decorate(this.textContextMenu_);
478 this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
479 this.gearButton_.addEventListener('menushow',
480 this.onShowGearMenu_.bind(this));
481 chrome.fileBrowserPrivate.onDesktopChanged.addListener(function() {
482 this.updateVisitDesktopMenus_();
483 this.ui_.updateProfileBadge();
485 chrome.fileBrowserPrivate.onProfileAdded.addListener(
486 this.updateVisitDesktopMenus_.bind(this));
487 this.updateVisitDesktopMenus_();
489 this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
491 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
493 this.syncButton.checkable = true;
494 this.hostedButton.checkable = true;
496 if (util.platform.runningInBrowser()) {
497 // Suppresses the default context menu.
498 this.dialogDom_.addEventListener('contextmenu', function(e) {
505 FileManager.prototype.onShowGearMenu_ = function() {
506 this.refreshRemainingSpace_(false); /* Without loading caption. */
508 // If the menu is opened while CTRL key pressed, secret menu itemscan be
510 this.isSecretGearMenuShown_ = this.pressingCtrl_;
512 // Update view of drive-related settings.
513 this.commandHandler.updateAvailability();
514 this.document_.getElementById('drive-separator').hidden =
515 !this.shouldShowDriveSettings();
517 // Force to update the gear menu position.
518 // TODO(hirono): Remove the workaround for the crbug.com/374093 after fixing
520 var gearMenu = this.document_.querySelector('#gear-menu');
521 gearMenu.style.left = '';
522 gearMenu.style.right = '';
523 gearMenu.style.top = '';
524 gearMenu.style.bottom = '';
528 * One-time initialization of commands.
531 FileManager.prototype.initCommands_ = function() {
532 this.commandHandler = new CommandHandler(this);
534 // TODO(hirono): Move the following block to the UI part.
535 var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
536 for (var j = 0; j < commandButtons.length; j++)
537 CommandButton.decorate(commandButtons[j]);
539 var inputs = this.dialogDom_.querySelectorAll(
540 'input[type=text], input[type=search], textarea');
541 for (var i = 0; i < inputs.length; i++) {
542 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
543 this.registerInputCommands_(inputs[i]);
546 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
547 this.textContextMenu_);
548 this.registerInputCommands_(this.renameInput_);
549 this.document_.addEventListener('command',
550 this.setNoHover_.bind(this, true));
554 * Registers cut, copy, paste and delete commands on input element.
556 * @param {Node} node Text input element to register on.
559 FileManager.prototype.registerInputCommands_ = function(node) {
560 CommandUtil.forceDefaultHandler(node, 'cut');
561 CommandUtil.forceDefaultHandler(node, 'copy');
562 CommandUtil.forceDefaultHandler(node, 'paste');
563 CommandUtil.forceDefaultHandler(node, 'delete');
564 node.addEventListener('keydown', function(e) {
565 var key = util.getKeyModifiers(e) + e.keyCode;
566 if (key === '190' /* '/' */ || key === '191' /* '.' */) {
567 // If this key event is propagated, this is handled search command,
568 // which calls 'preventDefault' method.
575 * Entry point of the initialization.
576 * This method is called from main.js.
578 FileManager.prototype.initializeCore = function() {
579 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
580 this.initializeQueue_.add(this.initBackgroundPage_.bind(this),
581 [], 'initBackgroundPage');
582 this.initializeQueue_.add(this.initPreferences_.bind(this),
583 ['initGeneral'], 'initPreferences');
584 this.initializeQueue_.add(this.initVolumeManager_.bind(this),
585 ['initGeneral', 'initBackgroundPage'],
586 'initVolumeManager');
588 this.initializeQueue_.run();
589 window.addEventListener('pagehide', this.onUnload_.bind(this));
592 FileManager.prototype.initializeUI = function(dialogDom, callback) {
593 this.dialogDom_ = dialogDom;
594 this.document_ = this.dialogDom_.ownerDocument;
596 this.initializeQueue_.add(
597 this.initEssentialUI_.bind(this),
598 ['initGeneral', 'initBackgroundPage'],
600 this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
601 ['initEssentialUI'], 'initAdditionalUI');
602 this.initializeQueue_.add(
603 this.initFileSystemUI_.bind(this),
604 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI');
606 // Run again just in case if all pending closures have completed and the
607 // queue has stopped and monitor the completion.
608 this.initializeQueue_.run(callback);
612 * Initializes general purpose basic things, which are used by other
613 * initializing methods.
615 * @param {function()} callback Completion callback.
618 FileManager.prototype.initGeneral_ = function(callback) {
619 // Initialize the application state.
620 // TODO(mtomasz): Unify window.appState with location.search format.
621 if (window.appState) {
622 this.params_ = window.appState.params || {};
623 this.initCurrentDirectoryURL_ = window.appState.currentDirectoryURL;
624 this.initSelectionURL_ = window.appState.selectionURL;
625 this.initTargetName_ = window.appState.targetName;
627 // Used by the select dialog only.
628 this.params_ = location.search ?
629 JSON.parse(decodeURIComponent(location.search.substr(1))) :
631 this.initCurrentDirectoryURL_ = this.params_.currentDirectoryURL;
632 this.initSelectionURL_ = this.params_.selectionURL;
633 this.initTargetName_ = this.params_.targetName;
636 // Initialize the member variables that depend this.params_.
637 this.dialogType = this.params_.type || DialogType.FULL_PAGE;
638 this.startupPrefName_ = 'file-manager-' + this.dialogType;
639 this.fileTypes_ = this.params_.typeList || [];
645 * Initialize the background page.
646 * @param {function()} callback Completion callback.
649 FileManager.prototype.initBackgroundPage_ = function(callback) {
650 chrome.runtime.getBackgroundPage(function(backgroundPage) {
651 this.backgroundPage_ = backgroundPage;
652 this.backgroundPage_.background.ready(function() {
653 loadTimeData.data = this.backgroundPage_.background.stringData;
654 if (util.platform.runningInBrowser()) {
655 this.backgroundPage_.registerDialog(window);
663 * Initializes the VolumeManager instance.
664 * @param {function()} callback Completion callback.
667 FileManager.prototype.initVolumeManager_ = function(callback) {
668 // Auto resolving to local path does not work for folders (e.g., dialog for
669 // loading unpacked extensions).
670 var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type);
672 // If this condition is false, VolumeManagerWrapper hides all drive
673 // related event and data, even if Drive is enabled on preference.
674 // In other words, even if Drive is disabled on preference but Files.app
675 // should show Drive when it is re-enabled, then the value should be set to
677 // Note that the Drive enabling preference change is listened by
678 // DriveIntegrationService, so here we don't need to take care about it.
680 !noLocalPathResolution || !this.params_.shouldReturnLocalPath;
681 this.volumeManager_ = new VolumeManagerWrapper(
682 driveEnabled, this.backgroundPage_);
687 * One time initialization of the Files.app's essential UI elements. These
688 * elements will be shown to the user. Only visible elements should be
689 * initialized here. Any heavy operation should be avoided. Files.app's
690 * window is shown at the end of this routine.
692 * @param {function()} callback Completion callback.
695 FileManager.prototype.initEssentialUI_ = function(callback) {
696 // Record stats of dialog types. New values must NOT be inserted into the
697 // array enumerating the types. It must be in sync with
698 // FileDialogType enum in tools/metrics/histograms/histogram.xml.
699 metrics.recordEnum('Create', this.dialogType,
700 [DialogType.SELECT_FOLDER,
701 DialogType.SELECT_UPLOAD_FOLDER,
702 DialogType.SELECT_SAVEAS_FILE,
703 DialogType.SELECT_OPEN_FILE,
704 DialogType.SELECT_OPEN_MULTI_FILE,
705 DialogType.FULL_PAGE]);
707 // Create the metadata cache.
708 this.metadataCache_ = MetadataCache.createFull(this.volumeManager_);
710 // Create the root view of FileManager.
711 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType);
712 this.fileTypeSelector_ = this.ui_.fileTypeSelector;
713 this.okButton_ = this.ui_.okButton;
714 this.cancelButton_ = this.ui_.cancelButton;
716 // Show the window as soon as the UI pre-initialization is done.
717 if (this.dialogType == DialogType.FULL_PAGE &&
718 !util.platform.runningInBrowser()) {
719 chrome.app.window.current().show();
720 setTimeout(callback, 100); // Wait until the animation is finished.
727 * One-time initialization of dialogs.
730 FileManager.prototype.initDialogs_ = function() {
731 // Initialize the dialog.
732 this.ui_.initDialogs();
733 FileManagerDialogBase.setFileManager(this);
735 // Obtains the dialog instances from FileManagerUI.
736 // TODO(hirono): Remove the properties from the FileManager class.
737 this.error = this.ui_.errorDialog;
738 this.alert = this.ui_.alertDialog;
739 this.confirm = this.ui_.confirmDialog;
740 this.prompt = this.ui_.promptDialog;
741 this.shareDialog_ = this.ui_.shareDialog;
742 this.defaultTaskPicker = this.ui_.defaultTaskPicker;
743 this.suggestAppsDialog = this.ui_.suggestAppsDialog;
747 * One-time initialization of various DOM nodes. Loads the additional DOM
748 * elements visible to the user. Initialize here elements, which are expensive
749 * or hidden in the beginning.
751 * @param {function()} callback Completion callback.
754 FileManager.prototype.initAdditionalUI_ = function(callback) {
756 this.ui_.initAdditionalUI();
758 this.dialogDom_.addEventListener('drop', function(e) {
759 // Prevent opening an URL by dropping it onto the page.
763 this.dialogDom_.addEventListener('click',
764 this.onExternalLinkClick_.bind(this));
765 // Cache nodes we'll be manipulating.
766 var dom = this.dialogDom_;
768 this.filenameInput_ = dom.querySelector('#filename-input-box input');
769 this.taskItems_ = dom.querySelector('#tasks');
771 this.table_ = dom.querySelector('.detail-table');
772 this.grid_ = dom.querySelector('.thumbnail-grid');
773 this.spinner_ = dom.querySelector('#list-container > .spinner-layer');
774 this.showSpinner_(true);
776 var fullPage = this.dialogType == DialogType.FULL_PAGE;
778 this.table_, this.metadataCache_, this.volumeManager_, fullPage);
779 FileGrid.decorate(this.grid_, this.metadataCache_, this.volumeManager_);
781 this.ui_.locationBreadcrumbs = new BreadcrumbsController(
782 dom.querySelector('#location-breadcrumbs'),
784 this.volumeManager_);
785 this.ui_.locationBreadcrumbs.addEventListener(
786 'pathclick', this.onBreadcrumbClick_.bind(this));
788 this.previewPanel_ = new PreviewPanel(
789 dom.querySelector('.preview-panel'),
790 DialogType.isOpenDialog(this.dialogType) ?
791 PreviewPanel.VisibilityType.ALWAYS_VISIBLE :
792 PreviewPanel.VisibilityType.AUTO,
794 this.volumeManager_);
795 this.previewPanel_.addEventListener(
796 PreviewPanel.Event.VISIBILITY_CHANGE,
797 this.onPreviewPanelVisibilityChange_.bind(this));
798 this.previewPanel_.initialize();
800 // Initialize progress center panel.
801 this.progressCenterPanel_ = new ProgressCenterPanel(
802 dom.querySelector('#progress-center'));
803 this.backgroundPage_.background.progressCenter.addPanel(
804 this.progressCenterPanel_);
806 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
807 this.document_.addEventListener('keyup', this.onKeyUp_.bind(this));
809 this.renameInput_ = this.document_.createElement('input');
810 this.renameInput_.className = 'rename entry-name';
812 this.renameInput_.addEventListener(
813 'keydown', this.onRenameInputKeyDown_.bind(this));
814 this.renameInput_.addEventListener(
815 'blur', this.onRenameInputBlur_.bind(this));
817 // TODO(hirono): Rename the handler after creating the DialogFooter class.
818 this.filenameInput_.addEventListener(
819 'input', this.onFilenameInputInput_.bind(this));
820 this.filenameInput_.addEventListener(
821 'keydown', this.onFilenameInputKeyDown_.bind(this));
822 this.filenameInput_.addEventListener(
823 'focus', this.onFilenameInputFocus_.bind(this));
825 this.listContainer_ = this.dialogDom_.querySelector('#list-container');
826 this.listContainer_.addEventListener(
827 'keydown', this.onListKeyDown_.bind(this));
828 this.listContainer_.addEventListener(
829 'keypress', this.onListKeyPress_.bind(this));
830 this.listContainer_.addEventListener(
831 'mousemove', this.onListMouseMove_.bind(this));
833 this.okButton_.addEventListener('click', this.onOk_.bind(this));
834 this.onCancelBound_ = this.onCancel_.bind(this);
835 this.cancelButton_.addEventListener('click', this.onCancelBound_);
837 this.decorateSplitter(
838 this.dialogDom_.querySelector('#navigation-list-splitter'));
840 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
842 this.syncButton = this.dialogDom_.querySelector(
843 '#gear-menu-drive-sync-settings');
844 this.hostedButton = this.dialogDom_.querySelector(
845 '#gear-menu-drive-hosted-settings');
847 this.ui_.toggleViewButton.addEventListener('click',
848 this.onToggleViewButtonClick_.bind(this));
850 cr.ui.ComboButton.decorate(this.taskItems_);
851 this.taskItems_.showMenu = function(shouldSetFocus) {
852 // Prevent the empty menu from opening.
853 if (!this.menu.length)
855 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus);
857 this.taskItems_.addEventListener('select',
858 this.onTaskItemClicked_.bind(this));
860 this.dialogDom_.ownerDocument.defaultView.addEventListener(
861 'resize', this.onResize_.bind(this));
863 this.filePopup_ = null;
865 this.searchBoxWrapper_ = this.ui_.searchBox.element;
866 this.searchBox_ = this.ui_.searchBox.inputElement;
867 this.searchBox_.addEventListener(
868 'input', this.onSearchBoxUpdate_.bind(this));
869 this.ui_.searchBox.clearButton.addEventListener(
870 'click', this.onSearchClearButtonClick_.bind(this));
872 this.lastSearchQuery_ = '';
874 this.autocompleteList_ = this.ui_.searchBox.autocompleteList;
875 this.autocompleteList_.requestSuggestions =
876 this.requestAutocompleteSuggestions_.bind(this);
878 // Instead, open the suggested item when Enter key is pressed or
880 this.autocompleteList_.handleEnterKeydown = function(event) {
881 this.openAutocompleteSuggestion_();
882 this.lastAutocompleteQuery_ = '';
883 this.autocompleteList_.suggestions = [];
885 this.autocompleteList_.addEventListener('mousedown', function(event) {
886 this.openAutocompleteSuggestion_();
887 this.lastAutocompleteQuery_ = '';
888 this.autocompleteList_.suggestions = [];
891 this.defaultActionMenuItem_ =
892 this.dialogDom_.querySelector('#default-action');
894 this.openWithCommand_ =
895 this.dialogDom_.querySelector('#open-with');
897 this.driveBuyMoreStorageCommand_ =
898 this.dialogDom_.querySelector('#drive-buy-more-space');
900 this.defaultActionMenuItem_.addEventListener('activate',
901 this.dispatchSelectionAction_.bind(this));
903 this.initFileTypeFilter_();
905 util.addIsFocusedMethod();
907 // Populate the static localized strings.
908 i18nTemplate.process(this.document_, loadTimeData);
910 // Arrange the file list.
911 this.table_.normalizeColumns();
912 this.table_.redraw();
918 * @param {Event} event Click event.
921 FileManager.prototype.onBreadcrumbClick_ = function(event) {
922 this.directoryModel_.changeDirectoryEntry(event.entry);
926 * Constructs table and grid (heavy operation).
929 FileManager.prototype.initFileList_ = function() {
930 // Always sharing the data model between the detail/thumb views confuses
931 // them. Instead we maintain this bogus data model, and hook it up to the
932 // view that is not in use.
933 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
934 this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
936 var singleSelection =
937 this.dialogType == DialogType.SELECT_OPEN_FILE ||
938 this.dialogType == DialogType.SELECT_FOLDER ||
939 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
940 this.dialogType == DialogType.SELECT_SAVEAS_FILE;
942 this.fileFilter_ = new FileFilter(
944 false /* Don't show dot files by default. */);
946 this.fileWatcher_ = new FileWatcher(this.metadataCache_);
947 this.fileWatcher_.addEventListener(
948 'watcher-metadata-changed',
949 this.onWatcherMetadataChanged_.bind(this));
951 this.directoryModel_ = new DirectoryModel(
956 this.volumeManager_);
958 this.folderShortcutsModel_ = new FolderShortcutsDataModel(
959 this.volumeManager_);
961 this.selectionHandler_ = new FileSelectionHandler(this);
963 var dataModel = this.directoryModel_.getFileList();
964 dataModel.addEventListener('permuted',
965 this.updateStartupPrefs_.bind(this));
967 this.directoryModel_.getFileListSelection().addEventListener('change',
968 this.selectionHandler_.onFileSelectionChanged.bind(
969 this.selectionHandler_));
971 this.initList_(this.grid_);
972 this.initList_(this.table_.list);
974 var fileListFocusBound = this.onFileListFocus_.bind(this);
975 this.table_.list.addEventListener('focus', fileListFocusBound);
976 this.grid_.addEventListener('focus', fileListFocusBound);
978 var draggingBound = this.onDragging_.bind(this);
979 var dragEndBound = this.onDragEnd_.bind(this);
981 // Listen to drag events to hide preview panel while user is dragging files.
982 // Files.app prevents default actions in 'dragstart' in some situations,
983 // so we listen to 'drag' to know the list is actually being dragged.
984 this.table_.list.addEventListener('drag', draggingBound);
985 this.grid_.addEventListener('drag', draggingBound);
986 this.table_.list.addEventListener('dragend', dragEndBound);
987 this.grid_.addEventListener('dragend', dragEndBound);
989 // Listen to dragselection events to hide preview panel while the user is
990 // selecting files by drag operation.
991 this.table_.list.addEventListener('dragselectionstart', draggingBound);
992 this.grid_.addEventListener('dragselectionstart', draggingBound);
993 this.table_.list.addEventListener('dragselectionend', dragEndBound);
994 this.grid_.addEventListener('dragselectionend', dragEndBound);
996 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
997 // attach the directory model.
998 this.initDirectoryTree_();
1000 this.table_.addEventListener('column-resize-end',
1001 this.updateStartupPrefs_.bind(this));
1003 // Restore preferences.
1004 this.directoryModel_.getFileList().sort(
1005 this.viewOptions_.sortField || 'modificationTime',
1006 this.viewOptions_.sortDirection || 'desc');
1007 if (this.viewOptions_.columns) {
1008 var cm = this.table_.columnModel;
1009 for (var i = 0; i < cm.totalSize; i++) {
1010 if (this.viewOptions_.columns[i] > 0)
1011 cm.setWidth(i, this.viewOptions_.columns[i]);
1014 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
1016 this.textSearchState_ = {text: '', date: new Date()};
1017 this.closeOnUnmount_ = (this.params_.action == 'auto-open');
1019 if (this.closeOnUnmount_) {
1020 this.volumeManager_.addEventListener('externally-unmounted',
1021 this.onExternallyUnmounted_.bind(this));
1024 // Update metadata to change 'Today' and 'Yesterday' dates.
1025 var today = new Date();
1027 today.setMinutes(0);
1028 today.setSeconds(0);
1029 today.setMilliseconds(0);
1030 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1031 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
1037 FileManager.prototype.initDirectoryTree_ = function() {
1038 var fakeEntriesVisible =
1039 this.dialogType !== DialogType.SELECT_SAVEAS_FILE;
1040 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
1041 DirectoryTree.decorate(this.directoryTree_,
1042 this.directoryModel_,
1043 this.volumeManager_,
1044 this.metadataCache_,
1045 fakeEntriesVisible);
1046 this.directoryTree_.dataModel = new NavigationListModel(
1047 this.volumeManager_, this.folderShortcutsModel_);
1049 // Visible height of the directory tree depends on the size of progress
1050 // center panel. When the size of progress center panel changes, directory
1051 // tree has to be notified to adjust its components (e.g. progress bar).
1052 var observer = new MutationObserver(
1053 this.directoryTree_.relayout.bind(this.directoryTree_));
1054 observer.observe(this.progressCenterPanel_.element,
1055 {subtree: true, attributes: true, childList: true});
1061 FileManager.prototype.updateStartupPrefs_ = function() {
1062 var sortStatus = this.directoryModel_.getFileList().sortStatus;
1064 sortField: sortStatus.field,
1065 sortDirection: sortStatus.direction,
1067 listType: this.listType_
1069 var cm = this.table_.columnModel;
1070 for (var i = 0; i < cm.totalSize; i++) {
1071 prefs.columns.push(cm.getWidth(i));
1073 // Save the global default.
1074 util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs));
1076 // Save the window-specific preference.
1077 if (window.appState) {
1078 window.appState.viewOptions = prefs;
1079 util.saveAppState();
1083 FileManager.prototype.refocus = function() {
1085 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
1086 targetElement = this.filenameInput_;
1088 targetElement = this.currentList_;
1090 // Hack: if the tabIndex is disabled, we can assume a modal dialog is
1091 // shown. Focus to a button on the dialog instead.
1092 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1)
1093 targetElement = document.querySelector('button:not([tabIndex="-1"])');
1096 targetElement.focus();
1100 * File list focus handler. Used to select the top most element on the list
1101 * if nothing was selected.
1105 FileManager.prototype.onFileListFocus_ = function() {
1106 // If the file list is focused by <Tab>, select the first item if no item
1108 if (this.pressingTab_) {
1109 if (this.getSelection() && this.getSelection().totalCount == 0)
1110 this.directoryModel_.selectIndex(0);
1115 * Index of selected item in the typeList of the dialog params.
1117 * @return {number} 1-based index of selected type or 0 if no type selected.
1120 FileManager.prototype.getSelectedFilterIndex_ = function() {
1121 var index = Number(this.fileTypeSelector_.selectedIndex);
1122 if (index < 0) // Nothing selected.
1124 if (this.params_.includeAllFiles) // Already 1-based.
1126 return index + 1; // Convert to 1-based;
1129 FileManager.prototype.setListType = function(type) {
1130 if (type && type == this.listType_)
1133 this.table_.list.startBatchUpdates();
1134 this.grid_.startBatchUpdates();
1136 // TODO(dzvorygin): style.display and dataModel setting order shouldn't
1137 // cause any UI bugs. Currently, the only right way is first to set display
1138 // style and only then set dataModel.
1140 if (type == FileManager.ListType.DETAIL) {
1141 this.table_.dataModel = this.directoryModel_.getFileList();
1142 this.table_.selectionModel = this.directoryModel_.getFileListSelection();
1143 this.table_.hidden = false;
1144 this.grid_.hidden = true;
1145 this.grid_.selectionModel = this.emptySelectionModel_;
1146 this.grid_.dataModel = this.emptyDataModel_;
1147 this.table_.hidden = false;
1148 /** @type {cr.ui.List} */
1149 this.currentList_ = this.table_.list;
1150 this.ui_.toggleViewButton.classList.remove('table');
1151 this.ui_.toggleViewButton.classList.add('grid');
1152 } else if (type == FileManager.ListType.THUMBNAIL) {
1153 this.grid_.dataModel = this.directoryModel_.getFileList();
1154 this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
1155 this.grid_.hidden = false;
1156 this.table_.hidden = true;
1157 this.table_.selectionModel = this.emptySelectionModel_;
1158 this.table_.dataModel = this.emptyDataModel_;
1159 this.grid_.hidden = false;
1160 /** @type {cr.ui.List} */
1161 this.currentList_ = this.grid_;
1162 this.ui_.toggleViewButton.classList.remove('grid');
1163 this.ui_.toggleViewButton.classList.add('table');
1165 throw new Error('Unknown list type: ' + type);
1168 this.listType_ = type;
1169 this.updateStartupPrefs_();
1172 this.table_.list.endBatchUpdates();
1173 this.grid_.endBatchUpdates();
1177 * Initialize the file list table or grid.
1179 * @param {cr.ui.List} list The list.
1182 FileManager.prototype.initList_ = function(list) {
1183 // Overriding the default role 'list' to 'listbox' for better accessibility
1185 list.setAttribute('role', 'listbox');
1186 list.addEventListener('click', this.onDetailClick_.bind(this));
1187 list.id = 'file-list';
1193 FileManager.prototype.onCopyProgress_ = function(event) {
1194 if (event.reason == 'ERROR' &&
1195 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
1196 event.error.data.toDrive &&
1197 event.error.data.name == util.FileError.QUOTA_EXCEEDED_ERR) {
1198 this.alert.showHtml(
1199 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
1200 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
1202 event.error.data.sourceFileUrl.split('/').pop()),
1203 str('GOOGLE_DRIVE_BUY_STORAGE_URL')));
1208 * Handler of file manager operations. Called when an entry has been
1210 * This updates directory model to reflect operation result immediately (not
1211 * waiting for directory update event). Also, preloads thumbnails for the
1212 * images of new entries.
1213 * See also FileOperationManager.EventRouter.
1215 * @param {Event} event An event for the entry change.
1218 FileManager.prototype.onEntriesChanged_ = function(event) {
1219 var kind = event.kind;
1220 var entries = event.entries;
1221 this.directoryModel_.onEntriesChanged(kind, entries);
1222 this.selectionHandler_.onFileSelectionChanged();
1224 if (kind !== util.EntryChangedKind.CREATED)
1227 var preloadThumbnail = function(entry) {
1228 var locationInfo = this.volumeManager_.getLocationInfo(entry);
1231 this.metadataCache_.getOne(entry, 'thumbnail|drive', function(metadata) {
1232 var thumbnailLoader_ = new ThumbnailLoader(
1234 ThumbnailLoader.LoaderType.CANVAS,
1236 undefined, // Media type.
1237 locationInfo.isDriveBased ?
1238 ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1239 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1240 10); // Very low priority.
1241 thumbnailLoader_.loadDetachedImage(function(success) {});
1245 for (var i = 0; i < entries.length; i++) {
1246 // Preload a thumbnail if the new copied entry an image.
1247 if (FileType.isImage(entries[i]))
1248 preloadThumbnail(entries[i]);
1253 * Fills the file type list or hides it.
1256 FileManager.prototype.initFileTypeFilter_ = function() {
1257 if (this.params_.includeAllFiles) {
1258 var option = this.document_.createElement('option');
1259 option.innerText = str('ALL_FILES_FILTER');
1260 this.fileTypeSelector_.appendChild(option);
1264 for (var i = 0; i !== this.fileTypes_.length; i++) {
1265 var fileType = this.fileTypes_[i];
1266 var option = this.document_.createElement('option');
1267 var description = fileType.description;
1269 // See if all the extensions in the group have the same description.
1270 for (var j = 0; j !== fileType.extensions.length; j++) {
1271 var currentDescription = FileType.typeToString(
1272 FileType.getTypeForName('.' + fileType.extensions[j]));
1273 if (!description) // Set the first time.
1274 description = currentDescription;
1275 else if (description != currentDescription) {
1276 // No single description, fall through to the extension list.
1283 // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1284 description = fileType.extensions.map(function(s) {
1288 option.innerText = description;
1290 option.value = i + 1;
1292 if (fileType.selected)
1293 option.selected = true;
1295 this.fileTypeSelector_.appendChild(option);
1298 var options = this.fileTypeSelector_.querySelectorAll('option');
1299 if (options.length >= 2) {
1300 // There is in fact no choice, show the selector.
1301 this.fileTypeSelector_.hidden = false;
1303 this.fileTypeSelector_.addEventListener('change',
1304 this.updateFileTypeFilter_.bind(this));
1309 * Filters file according to the selected file type.
1312 FileManager.prototype.updateFileTypeFilter_ = function() {
1313 this.fileFilter_.removeFilter('fileType');
1314 var selectedIndex = this.getSelectedFilterIndex_();
1315 if (selectedIndex > 0) { // Specific filter selected.
1316 var regexp = new RegExp('\\.(' +
1317 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
1318 var filter = function(entry) {
1319 return entry.isDirectory || regexp.test(entry.name);
1321 this.fileFilter_.addFilter('fileType', filter);
1323 // In save dialog, update the destination name extension.
1324 if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
1325 var current = this.filenameInput_.value;
1326 var newExt = this.fileTypes_[selectedIndex - 1].extensions[0];
1327 if (newExt && !regexp.test(current)) {
1328 var i = current.lastIndexOf('.');
1330 this.filenameInput_.value = current.substr(0, i) + '.' + newExt;
1331 this.selectTargetNameInFilenameInput_();
1339 * Resize details and thumb views to fit the new window size.
1342 FileManager.prototype.onResize_ = function() {
1343 if (this.listType_ == FileManager.ListType.THUMBNAIL)
1344 this.grid_.relayout();
1346 this.table_.relayout();
1348 // May not be available during initialization.
1349 if (this.directoryTree_)
1350 this.directoryTree_.relayout();
1352 this.ui_.locationBreadcrumbs.truncate();
1356 * Handles local metadata changes in the currect directory.
1357 * @param {Event} event Change event.
1360 FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
1361 this.updateMetadataInUI_(
1362 event.metadataType, event.entries, event.properties);
1366 * Resize details and thumb views to fit the new window size.
1369 FileManager.prototype.onPreviewPanelVisibilityChange_ = function() {
1370 // This method may be called on initialization. Some object may not be
1373 var panelHeight = this.previewPanel_.visible ?
1374 this.previewPanel_.height : 0;
1376 this.grid_.setBottomMarginForPanel(panelHeight);
1378 this.table_.setBottomMarginForPanel(panelHeight);
1382 * Invoked while the drag is being performed on the list or the grid.
1383 * Note: this method may be called multiple times before onDragEnd_().
1386 FileManager.prototype.onDragging_ = function() {
1387 // On open file dialog, the preview panel is always shown.
1388 if (DialogType.isOpenDialog(this.dialogType))
1390 this.previewPanel_.visibilityType =
1391 PreviewPanel.VisibilityType.ALWAYS_HIDDEN;
1395 * Invoked when the drag is ended on the list or the grid.
1398 FileManager.prototype.onDragEnd_ = function() {
1399 // On open file dialog, the preview panel is always shown.
1400 if (DialogType.isOpenDialog(this.dialogType))
1402 this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO;
1406 * Sets up the current directory during initialization.
1409 FileManager.prototype.setupCurrentDirectory_ = function() {
1410 var tracker = this.directoryModel_.createDirectoryChangeTracker();
1411 var queue = new AsyncUtil.Queue();
1413 // Wait until the volume manager is initialized.
1414 queue.run(function(callback) {
1416 this.volumeManager_.ensureInitialized(callback);
1419 var nextCurrentDirEntry;
1422 // Resolve the selectionURL to selectionEntry or to currentDirectoryEntry
1423 // in case of being a display root or a default directory to open files.
1424 queue.run(function(callback) {
1425 if (!this.initSelectionURL_) {
1429 webkitResolveLocalFileSystemURL(
1430 this.initSelectionURL_,
1432 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1433 // If location information is not available, then the volume is
1434 // no longer (or never) available.
1435 if (!locationInfo) {
1439 // If the selection is root, then use it as a current directory
1440 // instead. This is because, selecting a root entry is done as
1442 if (locationInfo.isRootEntry)
1443 nextCurrentDirEntry = inEntry;
1445 // If this dialog attempts to open file(s) and the selection is a
1446 // directory, the selection should be the current directory.
1447 if (DialogType.isOpenFileDialog(this.dialogType) &&
1448 inEntry.isDirectory)
1449 nextCurrentDirEntry = inEntry;
1451 // By default, the selection should be selected entry and the
1452 // parent directory of it should be the current directory.
1453 if (!nextCurrentDirEntry)
1454 selectionEntry = inEntry;
1457 }.bind(this), callback);
1459 // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done
1460 // by the previous step).
1461 queue.run(function(callback) {
1462 if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) {
1466 webkitResolveLocalFileSystemURL(
1467 this.initCurrentDirectoryURL_,
1469 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1470 if (!locationInfo) {
1474 nextCurrentDirEntry = inEntry;
1476 }.bind(this), callback);
1477 // TODO(mtomasz): Implement reopening on special search, when fake
1478 // entries are converted to directory providers.
1481 // If the directory to be changed to is not available, then first fallback
1482 // to the parent of the selection entry.
1483 queue.run(function(callback) {
1484 if (nextCurrentDirEntry || !selectionEntry) {
1488 selectionEntry.getParent(function(inEntry) {
1489 nextCurrentDirEntry = inEntry;
1494 // If the directory to be changed to is still not resolved, then fallback
1495 // to the default display root.
1496 queue.run(function(callback) {
1497 if (nextCurrentDirEntry) {
1501 this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) {
1502 nextCurrentDirEntry = displayRoot;
1507 // If selection failed to be resolved (eg. didn't exist, in case of saving
1508 // a file, or in case of a fallback of the current directory, then try to
1509 // resolve again using the target name.
1510 queue.run(function(callback) {
1511 if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) {
1515 // Try to resolve as a file first. If it fails, then as a directory.
1516 nextCurrentDirEntry.getFile(
1517 this.initTargetName_,
1519 function(targetEntry) {
1520 selectionEntry = targetEntry;
1523 // Failed to resolve as a file
1524 nextCurrentDirEntry.getDirectory(
1525 this.initTargetName_,
1527 function(targetEntry) {
1528 selectionEntry = targetEntry;
1531 // Failed to resolve as either file or directory.
1539 queue.run(function(callback) {
1540 // Check directory change.
1542 if (tracker.hasChanged) {
1546 // Finish setup current directory.
1547 this.finishSetupCurrentDirectory_(
1548 nextCurrentDirEntry,
1550 this.initTargetName_);
1556 * @param {DirectoryEntry} directoryEntry Directory to be opened.
1557 * @param {Entry=} opt_selectionEntry Entry to be selected.
1558 * @param {string=} opt_suggestedName Suggested name for a non-existing\
1562 FileManager.prototype.finishSetupCurrentDirectory_ = function(
1563 directoryEntry, opt_selectionEntry, opt_suggestedName) {
1564 // Open the directory, and select the selection (if passed).
1565 if (util.isFakeEntry(directoryEntry)) {
1566 this.directoryModel_.specialSearch(directoryEntry, '');
1568 this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
1569 if (opt_selectionEntry)
1570 this.directoryModel_.selectEntry(opt_selectionEntry);
1574 if (this.dialogType === DialogType.FULL_PAGE) {
1575 // In the FULL_PAGE mode if the restored URL points to a file we might
1576 // have to invoke a task after selecting it.
1577 if (this.params_.action === 'select')
1581 // Handle restoring after crash, or the gallery action.
1582 // TODO(mtomasz): Use the gallery action instead of just the gallery
1584 if (this.params_.gallery ||
1585 this.params_.action === 'gallery' ||
1586 this.params_.action === 'gallery-video') {
1587 if (!opt_selectionEntry) {
1588 // Non-existent file or a directory.
1589 // Reloading while the Gallery is open with empty or multiple
1590 // selection. Open the Gallery when the directory is scanned.
1592 new FileTasks(this, this.params_).openGallery([]);
1595 // The file or the directory exists.
1597 new FileTasks(this, this.params_).openGallery([opt_selectionEntry]);
1601 // TODO(mtomasz): Implement remounting archives after crash.
1602 // See: crbug.com/333139
1605 // If there is a task to be run, run it after the scan is completed.
1607 var listener = function() {
1608 if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(),
1610 // Opened on a different URL. Probably fallbacked. Therefore,
1611 // do not invoke a task.
1614 this.directoryModel_.removeEventListener(
1615 'scan-completed', listener);
1618 this.directoryModel_.addEventListener('scan-completed', listener);
1620 } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
1621 this.filenameInput_.value = opt_suggestedName || '';
1622 this.selectTargetNameInFilenameInput_();
1629 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
1630 var entries = this.directoryModel_.getFileList().slice();
1631 var directoryEntry = this.directoryModel_.getCurrentDirEntry();
1632 if (!directoryEntry)
1634 // We don't pass callback here. When new metadata arrives, we have an
1635 // observer registered to update the UI.
1637 // TODO(dgozman): refresh content metadata only when modificationTime
1639 var isFakeEntry = util.isFakeEntry(directoryEntry);
1640 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
1642 this.metadataCache_.clearRecursively(directoryEntry, '*');
1643 this.metadataCache_.get(getEntries, 'filesystem', null);
1645 if (this.isOnDrive())
1646 this.metadataCache_.get(getEntries, 'drive', null);
1648 var visibleItems = this.currentList_.items;
1649 var visibleEntries = [];
1650 for (var i = 0; i < visibleItems.length; i++) {
1651 var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
1652 var entry = this.directoryModel_.getFileList().item(index);
1653 // The following check is a workaround for the bug in list: sometimes item
1654 // does not have listIndex, and therefore is not found in the list.
1655 if (entry) visibleEntries.push(entry);
1657 this.metadataCache_.get(visibleEntries, 'thumbnail', null);
1663 FileManager.prototype.dailyUpdateModificationTime_ = function() {
1664 var entries = this.directoryModel_.getFileList().slice();
1665 this.metadataCache_.get(
1669 this.updateMetadataInUI_('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.
1681 FileManager.prototype.updateMetadataInUI_ = function(type, entries) {
1682 if (this.listType_ == FileManager.ListType.DETAIL)
1683 this.table_.updateListItemsMetadata(type, entries);
1685 this.grid_.updateListItemsMetadata(type, entries);
1686 // TODO: update bottom panel thumbnails.
1690 * Restore the item which is being renamed while refreshing the file list. Do
1691 * nothing if no item is being renamed or such an item disappeared.
1693 * While refreshing file list it gets repopulated with new file entries.
1694 * There is not a big difference whether DOM items stay the same or not.
1695 * Except for the item that the user is renaming.
1699 FileManager.prototype.restoreItemBeingRenamed_ = function() {
1700 if (!this.isRenamingInProgress())
1703 var dm = this.directoryModel_;
1704 var leadIndex = dm.getFileListSelection().leadIndex;
1708 var leadEntry = dm.getFileList().item(leadIndex);
1709 if (!util.isSameEntry(this.renameInput_.currentEntry, leadEntry))
1712 var leadListItem = this.findListItemForNode_(this.renameInput_);
1713 if (this.currentList_ == this.table_.list) {
1714 this.table_.updateFileMetadata(leadListItem, leadEntry);
1716 this.currentList_.restoreLeadItem(leadListItem);
1720 * TODO(mtomasz): Move this to a utility function working on the root type.
1721 * @return {boolean} True if the current directory content is from Google
1724 FileManager.prototype.isOnDrive = function() {
1725 var rootType = this.directoryModel_.getCurrentRootType();
1726 return rootType === VolumeManagerCommon.RootType.DRIVE ||
1727 rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME ||
1728 rootType === VolumeManagerCommon.RootType.DRIVE_RECENT ||
1729 rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE;
1733 * Check if the drive-related setting items should be shown on currently
1734 * displayed gear menu.
1735 * @return {boolean} True if those setting items should be shown.
1737 FileManager.prototype.shouldShowDriveSettings = function() {
1738 return this.isOnDrive() && this.isSecretGearMenuShown_;
1742 * Overrides default handling for clicks on hyperlinks.
1743 * In a packaged apps links with targer='_blank' open in a new tab by
1744 * default, other links do not open at all.
1746 * @param {Event} event Click event.
1749 FileManager.prototype.onExternalLinkClick_ = function(event) {
1750 if (event.target.tagName != 'A' || !event.target.href)
1753 if (this.dialogType != DialogType.FULL_PAGE)
1758 * Task combobox handler.
1760 * @param {Object} event Event containing task which was clicked.
1763 FileManager.prototype.onTaskItemClicked_ = function(event) {
1764 var selection = this.getSelection();
1765 if (!selection.tasks) return;
1767 if (event.item.task) {
1768 // Task field doesn't exist on change-default dropdown item.
1769 selection.tasks.execute(event.item.task.taskId);
1771 var extensions = [];
1773 for (var i = 0; i < selection.entries.length; i++) {
1774 var match = /\.(\w+)$/g.exec(selection.entries[i].toURL());
1776 var ext = match[1].toUpperCase();
1777 if (extensions.indexOf(ext) == -1) {
1778 extensions.push(ext);
1785 if (extensions.length == 1) {
1786 format = extensions[0];
1789 // Change default was clicked. We should open "change default" dialog.
1790 selection.tasks.showTaskPicker(this.defaultTaskPicker,
1791 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
1792 strf('CHANGE_DEFAULT_CAPTION', format),
1793 this.onDefaultTaskDone_.bind(this));
1798 * Sets the given task as default, when this task is applicable.
1800 * @param {Object} task Task to set as default.
1803 FileManager.prototype.onDefaultTaskDone_ = function(task) {
1804 // TODO(dgozman): move this method closer to tasks.
1805 var selection = this.getSelection();
1806 chrome.fileBrowserPrivate.setDefaultTask(
1808 util.entriesToURLs(selection.entries),
1809 selection.mimeTypes);
1810 selection.tasks = new FileTasks(this);
1811 selection.tasks.init(selection.entries, selection.mimeTypes);
1812 selection.tasks.display(this.taskItems_);
1813 this.refreshCurrentDirectoryMetadata_();
1814 this.selectionHandler_.onFileSelectionChanged();
1820 FileManager.prototype.onPreferencesChanged_ = function() {
1822 this.getPreferences_(function(prefs) {
1823 self.initDateTimeFormatters_();
1824 self.refreshCurrentDirectoryMetadata_();
1826 if (prefs.cellularDisabled)
1827 self.syncButton.setAttribute('checked', '');
1829 self.syncButton.removeAttribute('checked');
1831 if (self.hostedButton.hasAttribute('checked') ===
1832 prefs.hostedFilesDisabled && self.isOnDrive()) {
1833 self.directoryModel_.rescan(false);
1836 if (!prefs.hostedFilesDisabled)
1837 self.hostedButton.setAttribute('checked', '');
1839 self.hostedButton.removeAttribute('checked');
1841 true /* refresh */);
1844 FileManager.prototype.onDriveConnectionChanged_ = function() {
1845 var connection = this.volumeManager_.getDriveConnectionState();
1846 if (this.commandHandler)
1847 this.commandHandler.updateAvailability();
1848 if (this.dialogContainer_)
1849 this.dialogContainer_.setAttribute('connection', connection.type);
1850 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
1851 this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
1855 * Tells whether the current directory is read only.
1856 * TODO(mtomasz): Remove and use EntryLocation directly.
1857 * @return {boolean} True if read only, false otherwise.
1859 FileManager.prototype.isOnReadonlyDirectory = function() {
1860 return this.directoryModel_.isReadOnly();
1864 * @param {Event} Unmount event.
1867 FileManager.prototype.onExternallyUnmounted_ = function(event) {
1868 if (event.volumeInfo === this.currentVolumeInfo_) {
1869 if (this.closeOnUnmount_) {
1870 // If the file manager opened automatically when a usb drive inserted,
1871 // user have never changed current volume (that implies the current
1872 // directory is still on the device) then close this window.
1879 * Shows a modal-like file viewer/editor on top of the File Manager UI.
1881 * @param {HTMLElement} popup Popup element.
1882 * @param {function()} closeCallback Function to call after the popup is
1885 FileManager.prototype.openFilePopup = function(popup, closeCallback) {
1886 this.closeFilePopup();
1887 this.filePopup_ = popup;
1888 this.filePopupCloseCallback_ = closeCallback;
1889 this.dialogDom_.insertBefore(
1890 this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area'));
1891 this.filePopup_.focus();
1892 this.document_.body.setAttribute('overlay-visible', '');
1893 this.document_.querySelector('#iframe-drag-area').hidden = false;
1897 * Closes the modal-like file viewer/editor popup.
1899 FileManager.prototype.closeFilePopup = function() {
1900 if (this.filePopup_) {
1901 this.document_.body.removeAttribute('overlay-visible');
1902 this.document_.querySelector('#iframe-drag-area').hidden = true;
1903 // The window resize would not be processed properly while the relevant
1904 // divs had 'display:none', force resize after the layout fired.
1905 setTimeout(this.onResize_.bind(this), 0);
1906 if (this.filePopup_.contentWindow &&
1907 this.filePopup_.contentWindow.unload) {
1908 this.filePopup_.contentWindow.unload();
1911 if (this.filePopupCloseCallback_) {
1912 this.filePopupCloseCallback_();
1913 this.filePopupCloseCallback_ = null;
1916 // These operations have to be in the end, otherwise v8 crashes on an
1917 // assert. See: crbug.com/224174.
1918 this.dialogDom_.removeChild(this.filePopup_);
1919 this.filePopup_ = null;
1924 * Updates visibility of the draggable app region in the modal-like file
1927 * @param {boolean} visible True for visible, false otherwise.
1929 FileManager.prototype.onFilePopupAppRegionChanged = function(visible) {
1930 if (!this.filePopup_)
1933 this.document_.querySelector('#iframe-drag-area').hidden = !visible;
1937 * @return {Array.<Entry>} List of all entries in the current directory.
1939 FileManager.prototype.getAllEntriesInCurrentDirectory = function() {
1940 return this.directoryModel_.getFileList().slice();
1943 FileManager.prototype.isRenamingInProgress = function() {
1944 return !!this.renameInput_.currentEntry;
1950 FileManager.prototype.focusCurrentList_ = function() {
1951 if (this.listType_ == FileManager.ListType.DETAIL)
1952 this.table_.focus();
1953 else // this.listType_ == FileManager.ListType.THUMBNAIL)
1958 * Return DirectoryEntry of the current directory or null.
1959 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
1960 * null if the directory model is not ready or the current directory is
1963 FileManager.prototype.getCurrentDirectoryEntry = function() {
1964 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
1968 * Shows the share dialog for the selected file or directory.
1970 FileManager.prototype.shareSelection = function() {
1971 var entries = this.getSelection().entries;
1972 if (entries.length != 1) {
1973 console.warn('Unable to share multiple items at once.');
1976 // Add the overlapped class to prevent the applicaiton window from
1977 // captureing mouse events.
1978 this.shareDialog_.show(entries[0], function(result) {
1979 if (result == ShareDialog.Result.NETWORK_ERROR)
1980 this.error.show(str('SHARE_ERROR'));
1985 * Creates a folder shortcut.
1986 * @param {Entry} entry A shortcut which refers to |entry| to be created.
1988 FileManager.prototype.createFolderShortcut = function(entry) {
1990 if (this.folderShortcutExists(entry))
1993 this.folderShortcutsModel_.add(entry);
1997 * Checkes if the shortcut which refers to the given folder exists or not.
1998 * @param {Entry} entry Entry of the folder to be checked.
2000 FileManager.prototype.folderShortcutExists = function(entry) {
2001 return this.folderShortcutsModel_.exists(entry);
2005 * Removes the folder shortcut.
2006 * @param {Entry} entry The shortcut which refers to |entry| is to be removed.
2008 FileManager.prototype.removeFolderShortcut = function(entry) {
2009 this.folderShortcutsModel_.remove(entry);
2013 * Blinks the selection. Used to give feedback when copying or cutting the
2016 FileManager.prototype.blinkSelection = function() {
2017 var selection = this.getSelection();
2018 if (!selection || selection.totalCount == 0)
2021 for (var i = 0; i < selection.entries.length; i++) {
2022 var selectedIndex = selection.indexes[i];
2023 var listItem = this.currentList_.getListItemByIndex(selectedIndex);
2025 this.blinkListItem_(listItem);
2030 * @param {Element} listItem List item element.
2033 FileManager.prototype.blinkListItem_ = function(listItem) {
2034 listItem.classList.add('blink');
2035 setTimeout(function() {
2036 listItem.classList.remove('blink');
2043 FileManager.prototype.selectTargetNameInFilenameInput_ = function() {
2044 var input = this.filenameInput_;
2046 var selectionEnd = input.value.lastIndexOf('.');
2047 if (selectionEnd == -1) {
2050 input.selectionStart = 0;
2051 input.selectionEnd = selectionEnd;
2056 * Handles mouse click or tap.
2058 * @param {Event} event The click event.
2061 FileManager.prototype.onDetailClick_ = function(event) {
2062 if (this.isRenamingInProgress()) {
2063 // Don't pay attention to clicks during a rename.
2067 var listItem = this.findListItemForEvent_(event);
2068 var selection = this.getSelection();
2069 if (!listItem || !listItem.selected || selection.totalCount != 1) {
2073 // React on double click, but only if both clicks hit the same item.
2074 // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2075 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
2076 this.lastClickedItem_ = listItem;
2078 if (event.detail != clickNumber)
2081 var entry = selection.entries[0];
2082 if (entry.isDirectory) {
2083 this.onDirectoryAction_(entry);
2085 this.dispatchSelectionAction_();
2092 FileManager.prototype.dispatchSelectionAction_ = function() {
2093 if (this.dialogType == DialogType.FULL_PAGE) {
2094 var selection = this.getSelection();
2095 var tasks = selection.tasks;
2096 var urls = selection.urls;
2097 var mimeTypes = selection.mimeTypes;
2099 tasks.executeDefault();
2102 if (!this.okButton_.disabled) {
2110 * Opens the suggest file dialog.
2112 * @param {Entry} entry Entry of the file.
2113 * @param {function()} onSuccess Success callback.
2114 * @param {function()} onCancelled User-cancelled callback.
2115 * @param {function()} onFailure Failure callback.
2118 FileManager.prototype.openSuggestAppsDialog =
2119 function(entry, onSuccess, onCancelled, onFailure) {
2125 this.metadataCache_.getOne(entry, 'drive', function(prop) {
2126 if (!prop || !prop.contentMimeType) {
2131 var basename = entry.name;
2132 var splitted = util.splitExtension(basename);
2133 var filename = splitted[0];
2134 var extension = splitted[1];
2135 var mime = prop.contentMimeType;
2137 // Returns with failure if the file has neither extension nor mime.
2138 if (!extension || !mime) {
2143 var onDialogClosed = function(result) {
2145 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
2148 case SuggestAppsDialog.Result.FAILED:
2156 if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
2157 this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
2159 this.suggestAppsDialog.showByExtensionAndMime(
2160 extension, mime, onDialogClosed);
2166 * Called when a dialog is shown or hidden.
2167 * @param {boolean} flag True if a dialog is shown, false if hidden.
2169 FileManager.prototype.onDialogShownOrHidden = function(show) {
2171 // If a dialog is shown, activate the window.
2172 var appWindow = chrome.app.window.current();
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_.label;
2207 * Updates the location information displayed on the toolbar.
2208 * @param {DirectoryEntry=} opt_entry Directory entry to be displayed as
2209 * current location. Default entry is the current directory.
2212 FileManager.prototype.updateLocationLine_ = function(opt_entry) {
2213 var entry = opt_entry || this.getCurrentDirectoryEntry();
2214 // Updates volume icon.
2215 var location = this.volumeManager_.getLocationInfo(entry);
2216 if (location && location.rootType && location.isRootEntry) {
2217 this.ui_.locationVolumeIcon.setAttribute(
2218 'volume-type-icon', location.rootType);
2219 this.ui_.locationVolumeIcon.removeAttribute('volume-subtype');
2221 this.ui_.locationVolumeIcon.setAttribute(
2222 'volume-type-icon', location.volumeInfo.volumeType);
2223 this.ui_.locationVolumeIcon.setAttribute(
2224 'volume-subtype', location.volumeInfo.deviceType);
2226 // Updates breadcrumbs.
2227 this.ui_.locationBreadcrumbs.show(entry);
2231 * Update the gear menu.
2234 FileManager.prototype.updateGearMenu_ = function() {
2235 this.refreshRemainingSpace_(true); // Show loading caption.
2239 * Update menus that move the window to the other profile's desktop.
2240 * TODO(hirono): Add the GearMenu class and make it a member of the class.
2241 * TODO(hirono): Handle the case where a profile is added while the menu is
2245 FileManager.prototype.updateVisitDesktopMenus_ = function() {
2246 var gearMenu = this.document_.querySelector('#gear-menu');
2248 this.document_.querySelector('#multi-profile-separator');
2250 // Remove existing menu items.
2252 this.document_.querySelectorAll('#gear-menu .visit-desktop');
2253 for (var i = 0; i < oldItems.length; i++) {
2254 gearMenu.removeChild(oldItems[i]);
2256 separator.hidden = true;
2258 if (this.dialogType !== DialogType.FULL_PAGE)
2261 // Obtain the profile information.
2262 chrome.fileBrowserPrivate.getProfiles(function(profiles,
2265 // Check if the menus are needed or not.
2266 var insertingPosition = separator.nextSibling;
2267 if (profiles.length === 1 && profiles[0].profileId === displayedId)
2270 separator.hidden = false;
2271 for (var i = 0; i < profiles.length; i++) {
2272 var profile = profiles[i];
2273 if (profile.profileId === displayedId)
2275 var item = this.document_.createElement('menuitem');
2276 cr.ui.MenuItem.decorate(item);
2277 gearMenu.insertBefore(item, insertingPosition);
2278 item.className = 'visit-desktop';
2279 item.label = strf('VISIT_DESKTOP_OF_USER',
2280 profile.displayName,
2282 item.addEventListener('activate', function(inProfile, event) {
2283 // Stop propagate and hide the menu manually, in order to prevent the
2284 // focus from being back to the button. (cf. http://crbug.com/248479)
2285 event.stopPropagation();
2286 this.gearButton_.hideMenu();
2287 this.gearButton_.blur();
2288 chrome.fileBrowserPrivate.visitDesktop(inProfile.profileId);
2289 }.bind(this, profile));
2295 * Refreshes space info of the current volume.
2296 * @param {boolean} showLoadingCaption Whether show loading caption or not.
2299 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
2300 if (!this.currentVolumeInfo_)
2303 var volumeSpaceInfo =
2304 this.dialogDom_.querySelector('#volume-space-info');
2305 var volumeSpaceInfoSeparator =
2306 this.dialogDom_.querySelector('#volume-space-info-separator');
2307 var volumeSpaceInfoLabel =
2308 this.dialogDom_.querySelector('#volume-space-info-label');
2309 var volumeSpaceInnerBar =
2310 this.dialogDom_.querySelector('#volume-space-info-bar');
2311 var volumeSpaceOuterBar =
2312 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
2314 var currentVolumeInfo = this.currentVolumeInfo_;
2316 // TODO(mtomasz): Add support for remaining space indication for provided
2318 if (currentVolumeInfo.volumeType ==
2319 VolumeManagerCommon.VolumeType.PROVIDED) {
2320 volumeSpaceInfo.hidden = true;
2321 volumeSpaceInfoSeparator.hidden = true;
2325 volumeSpaceInfo.hidden = false;
2326 volumeSpaceInfoSeparator.hidden = false;
2327 volumeSpaceInnerBar.setAttribute('pending', '');
2329 if (showLoadingCaption) {
2330 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2331 volumeSpaceInnerBar.style.width = '100%';
2334 chrome.fileBrowserPrivate.getSizeStats(
2335 currentVolumeInfo.volumeId, function(result) {
2336 var volumeInfo = this.volumeManager_.getVolumeInfo(
2337 this.directoryModel_.getCurrentDirEntry());
2338 if (currentVolumeInfo !== this.currentVolumeInfo_)
2340 updateSpaceInfo(result,
2341 volumeSpaceInnerBar,
2342 volumeSpaceInfoLabel,
2343 volumeSpaceOuterBar);
2348 * Update the UI when the current directory changes.
2350 * @param {Event} event The directory-changed event.
2353 FileManager.prototype.onDirectoryChanged_ = function(event) {
2354 var oldCurrentVolumeInfo = this.currentVolumeInfo_;
2356 // Remember the current volume info.
2357 this.currentVolumeInfo_ = this.volumeManager_.getVolumeInfo(
2360 // If volume has changed, then update the gear menu.
2361 if (oldCurrentVolumeInfo !== this.currentVolumeInfo_) {
2362 this.updateGearMenu_();
2363 // If the volume has changed, and it was previously set, then do not
2364 // close on unmount anymore.
2365 if (oldCurrentVolumeInfo)
2366 this.closeOnUnmount_ = false;
2369 this.selectionHandler_.onFileSelectionChanged();
2370 this.ui_.searchBox.clear();
2371 // TODO(mtomasz): Consider remembering the selection.
2372 util.updateAppState(
2373 this.getCurrentDirectoryEntry() ?
2374 this.getCurrentDirectoryEntry().toURL() : '',
2375 '' /* selectionURL */,
2376 '' /* opt_param */);
2378 if (this.commandHandler)
2379 this.commandHandler.updateAvailability();
2381 this.updateUnformattedVolumeStatus_();
2382 this.updateTitle_();
2383 this.updateLocationLine_();
2385 var currentEntry = this.getCurrentDirectoryEntry();
2386 this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ?
2387 null : currentEntry;
2390 FileManager.prototype.updateUnformattedVolumeStatus_ = function() {
2391 var volumeInfo = this.volumeManager_.getVolumeInfo(
2392 this.directoryModel_.getCurrentDirEntry());
2394 if (volumeInfo && volumeInfo.error) {
2395 this.dialogDom_.setAttribute('unformatted', '');
2397 var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2398 if (volumeInfo.error ===
2399 VolumeManagerCommon.VolumeError.UNSUPPORTED_FILESYSTEM) {
2400 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2402 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2405 // Update 'canExecute' for format command so the format button's disabled
2406 // property is properly set.
2407 if (this.commandHandler)
2408 this.commandHandler.updateAvailability();
2410 this.dialogDom_.removeAttribute('unformatted');
2414 FileManager.prototype.findListItemForEvent_ = function(event) {
2415 return this.findListItemForNode_(event.touchedElement || event.srcElement);
2418 FileManager.prototype.findListItemForNode_ = function(node) {
2419 var item = this.currentList_.getListItemAncestor(node);
2420 // TODO(serya): list should check that.
2421 return item && this.currentList_.isItem(item) ? item : null;
2425 * Unload handler for the page.
2428 FileManager.prototype.onUnload_ = function() {
2429 if (this.directoryModel_)
2430 this.directoryModel_.dispose();
2431 if (this.volumeManager_)
2432 this.volumeManager_.dispose();
2433 if (this.filePopup_ &&
2434 this.filePopup_.contentWindow &&
2435 this.filePopup_.contentWindow.unload)
2436 this.filePopup_.contentWindow.unload(true /* exiting */);
2437 if (this.progressCenterPanel_)
2438 this.backgroundPage_.background.progressCenter.removePanel(
2439 this.progressCenterPanel_);
2440 if (this.fileOperationManager_) {
2441 if (this.onCopyProgressBound_) {
2442 this.fileOperationManager_.removeEventListener(
2443 'copy-progress', this.onCopyProgressBound_);
2445 if (this.onEntriesChangedBound_) {
2446 this.fileOperationManager_.removeEventListener(
2447 'entries-changed', this.onEntriesChangedBound_);
2450 window.closing = true;
2451 if (this.backgroundPage_)
2452 this.backgroundPage_.background.tryClose();
2455 FileManager.prototype.initiateRename = function() {
2456 var item = this.currentList_.ensureLeadItemExists();
2459 var label = item.querySelector('.filename-label');
2460 var input = this.renameInput_;
2461 var currentEntry = this.currentList_.dataModel.item(item.listIndex);
2463 input.value = label.textContent;
2464 item.setAttribute('renaming', '');
2465 label.parentNode.appendChild(input);
2468 var selectionEnd = input.value.lastIndexOf('.');
2469 if (currentEntry.isFile && selectionEnd !== -1) {
2470 input.selectionStart = 0;
2471 input.selectionEnd = selectionEnd;
2476 // This has to be set late in the process so we don't handle spurious
2478 input.currentEntry = currentEntry;
2479 this.table_.startBatchUpdates();
2480 this.grid_.startBatchUpdates();
2484 * @type {Event} Key event.
2487 FileManager.prototype.onRenameInputKeyDown_ = function(event) {
2488 if (!this.isRenamingInProgress())
2491 // Do not move selection or lead item in list during rename.
2492 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
2493 event.stopPropagation();
2496 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
2497 case 'U+001B': // Escape
2498 this.cancelRename_();
2499 event.preventDefault();
2503 this.commitRename_();
2504 event.preventDefault();
2510 * @type {Event} Blur event.
2513 FileManager.prototype.onRenameInputBlur_ = function(event) {
2514 if (this.isRenamingInProgress() && !this.renameInput_.validation_)
2515 this.commitRename_();
2521 FileManager.prototype.commitRename_ = function() {
2522 var input = this.renameInput_;
2523 var entry = input.currentEntry;
2524 var newName = input.value;
2526 if (newName == entry.name) {
2527 this.cancelRename_();
2531 var renamedItemElement = this.findListItemForNode_(this.renameInput_);
2532 var nameNode = renamedItemElement.querySelector('.filename-label');
2534 input.validation_ = true;
2535 var validationDone = function(valid) {
2536 input.validation_ = false;
2539 // Cancel rename if it fails to restore focus from alert dialog.
2540 // Otherwise, just cancel the commitment and continue to rename.
2541 if (this.document_.activeElement != input)
2542 this.cancelRename_();
2546 // Validation succeeded. Do renaming.
2547 this.renameInput_.currentEntry = null;
2548 if (this.renameInput_.parentNode)
2549 this.renameInput_.parentNode.removeChild(this.renameInput_);
2550 renamedItemElement.setAttribute('renaming', 'provisional');
2552 // Optimistically apply new name immediately to avoid flickering in
2554 nameNode.textContent = newName;
2558 function(newEntry) {
2559 this.directoryModel_.onRenameEntry(entry, newEntry);
2560 renamedItemElement.removeAttribute('renaming');
2561 this.table_.endBatchUpdates();
2562 this.grid_.endBatchUpdates();
2563 // Focus may go out of the list. Back it to the list.
2564 this.currentList_.focus();
2567 // Write back to the old name.
2568 nameNode.textContent = entry.name;
2569 renamedItemElement.removeAttribute('renaming');
2570 this.table_.endBatchUpdates();
2571 this.grid_.endBatchUpdates();
2573 // Show error dialog.
2575 if (error.name == util.FileError.PATH_EXISTS_ERR ||
2576 error.name == util.FileError.TYPE_MISMATCH_ERR) {
2577 // Check the existing entry is file or not.
2578 // 1) If the entry is a file:
2579 // a) If we get PATH_EXISTS_ERR, a file exists.
2580 // b) If we get TYPE_MISMATCH_ERR, a directory exists.
2581 // 2) If the entry is a directory:
2582 // a) If we get PATH_EXISTS_ERR, a directory exists.
2583 // b) If we get TYPE_MISMATCH_ERR, a file exists.
2585 (entry.isFile && error.name ==
2586 util.FileError.PATH_EXISTS_ERR) ||
2587 (!entry.isFile && error.name ==
2588 util.FileError.TYPE_MISMATCH_ERR) ?
2589 'FILE_ALREADY_EXISTS' :
2590 'DIRECTORY_ALREADY_EXISTS',
2593 message = strf('ERROR_RENAMING', entry.name,
2594 util.getFileErrorString(error.name));
2597 this.alert.show(message);
2601 // TODO(haruki): this.getCurrentDirectoryEntry() might not return the actual
2602 // parent if the directory content is a search result. Fix it to do proper
2604 this.validateFileName_(this.getCurrentDirectoryEntry(),
2606 validationDone.bind(this));
2612 FileManager.prototype.cancelRename_ = function() {
2613 this.renameInput_.currentEntry = null;
2615 var item = this.findListItemForNode_(this.renameInput_);
2617 item.removeAttribute('renaming');
2619 var parent = this.renameInput_.parentNode;
2621 parent.removeChild(this.renameInput_);
2623 this.table_.endBatchUpdates();
2624 this.grid_.endBatchUpdates();
2626 // Focus may go out of the list. Back it to the list.
2627 this.currentList_.focus();
2631 * @param {Event} Key event.
2634 FileManager.prototype.onFilenameInputInput_ = function() {
2635 this.selectionHandler_.updateOkButton();
2639 * @param {Event} Key event.
2642 FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
2643 if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */)
2644 this.okButton_.click();
2648 * @param {Event} Focus event.
2651 FileManager.prototype.onFilenameInputFocus_ = function(event) {
2652 var input = this.filenameInput_;
2654 // On focus we want to select everything but the extension, but
2655 // Chrome will select-all after the focus event completes. We
2656 // schedule a timeout to alter the focus after that happens.
2657 setTimeout(function() {
2658 var selectionEnd = input.value.lastIndexOf('.');
2659 if (selectionEnd == -1) {
2662 input.selectionStart = 0;
2663 input.selectionEnd = selectionEnd;
2671 FileManager.prototype.onScanStarted_ = function() {
2672 if (this.scanInProgress_) {
2673 this.table_.list.endBatchUpdates();
2674 this.grid_.endBatchUpdates();
2677 if (this.commandHandler)
2678 this.commandHandler.updateAvailability();
2679 this.table_.list.startBatchUpdates();
2680 this.grid_.startBatchUpdates();
2681 this.scanInProgress_ = true;
2683 this.scanUpdatedAtLeastOnceOrCompleted_ = false;
2684 if (this.scanCompletedTimer_) {
2685 clearTimeout(this.scanCompletedTimer_);
2686 this.scanCompletedTimer_ = null;
2689 if (this.scanUpdatedTimer_) {
2690 clearTimeout(this.scanUpdatedTimer_);
2691 this.scanUpdatedTimer_ = null;
2694 if (this.spinner_.hidden) {
2695 this.cancelSpinnerTimeout_();
2696 this.showSpinnerTimeout_ =
2697 setTimeout(this.showSpinner_.bind(this, true), 500);
2704 FileManager.prototype.onScanCompleted_ = function() {
2705 if (!this.scanInProgress_) {
2706 console.error('Scan-completed event recieved. But scan is not started.');
2710 if (this.commandHandler)
2711 this.commandHandler.updateAvailability();
2712 this.hideSpinnerLater_();
2714 if (this.scanUpdatedTimer_) {
2715 clearTimeout(this.scanUpdatedTimer_);
2716 this.scanUpdatedTimer_ = null;
2719 // To avoid flickering postpone updating the ui by a small amount of time.
2720 // There is a high chance, that metadata will be received within 50 ms.
2721 this.scanCompletedTimer_ = setTimeout(function() {
2722 // Check if batch updates are already finished by onScanUpdated_().
2723 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2724 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2727 this.scanInProgress_ = false;
2728 this.table_.list.endBatchUpdates();
2729 this.grid_.endBatchUpdates();
2730 this.scanCompletedTimer_ = null;
2737 FileManager.prototype.onScanUpdated_ = function() {
2738 if (!this.scanInProgress_) {
2739 console.error('Scan-updated event recieved. But scan is not started.');
2743 if (this.scanUpdatedTimer_ || this.scanCompletedTimer_)
2746 // Show contents incrementally by finishing batch updated, but only after
2747 // 200ms elapsed, to avoid flickering when it is not necessary.
2748 this.scanUpdatedTimer_ = setTimeout(function() {
2749 // We need to hide the spinner only once.
2750 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2751 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2752 this.hideSpinnerLater_();
2756 if (this.scanInProgress_) {
2757 this.table_.list.endBatchUpdates();
2758 this.grid_.endBatchUpdates();
2759 this.table_.list.startBatchUpdates();
2760 this.grid_.startBatchUpdates();
2762 this.scanUpdatedTimer_ = null;
2769 FileManager.prototype.onScanCancelled_ = function() {
2770 if (!this.scanInProgress_) {
2771 console.error('Scan-cancelled event recieved. But scan is not started.');
2775 if (this.commandHandler)
2776 this.commandHandler.updateAvailability();
2777 this.hideSpinnerLater_();
2778 if (this.scanCompletedTimer_) {
2779 clearTimeout(this.scanCompletedTimer_);
2780 this.scanCompletedTimer_ = null;
2782 if (this.scanUpdatedTimer_) {
2783 clearTimeout(this.scanUpdatedTimer_);
2784 this.scanUpdatedTimer_ = null;
2786 // Finish unfinished batch updates.
2787 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2788 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2791 this.scanInProgress_ = false;
2792 this.table_.list.endBatchUpdates();
2793 this.grid_.endBatchUpdates();
2797 * Handle the 'rescan-completed' from the DirectoryModel.
2800 FileManager.prototype.onRescanCompleted_ = function() {
2801 this.selectionHandler_.onFileSelectionChanged();
2807 FileManager.prototype.cancelSpinnerTimeout_ = function() {
2808 if (this.showSpinnerTimeout_) {
2809 clearTimeout(this.showSpinnerTimeout_);
2810 this.showSpinnerTimeout_ = null;
2817 FileManager.prototype.hideSpinnerLater_ = function() {
2818 this.cancelSpinnerTimeout_();
2819 this.showSpinner_(false);
2823 * @param {boolean} on True to show, false to hide.
2826 FileManager.prototype.showSpinner_ = function(on) {
2827 if (on && this.directoryModel_ && this.directoryModel_.isScanning())
2828 this.spinner_.hidden = false;
2830 if (!on && (!this.directoryModel_ ||
2831 !this.directoryModel_.isScanning() ||
2832 this.directoryModel_.getFileList().length != 0)) {
2833 this.spinner_.hidden = true;
2837 FileManager.prototype.createNewFolder = function() {
2838 var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
2840 // Find a name that doesn't exist in the data model.
2841 var files = this.directoryModel_.getFileList();
2843 for (var i = 0; i < files.length; i++) {
2844 var name = files.item(i).name;
2845 // Filtering names prevents from conflicts with prototype's names
2847 if (name.substring(0, defaultName.length) == defaultName)
2851 var baseName = defaultName;
2856 var advance = function() {
2862 var current = function() {
2863 return baseName + separator + index + suffix;
2866 // Accessing hasOwnProperty is safe since hash properties filtered.
2867 while (hash.hasOwnProperty(current())) {
2872 var list = self.currentList_;
2873 var tryCreate = function() {
2876 var onSuccess = function(entry) {
2877 metrics.recordUserAction('CreateNewFolder');
2878 list.selectedItem = entry;
2880 self.table_.list.endBatchUpdates();
2881 self.grid_.endBatchUpdates();
2883 self.initiateRename();
2886 var onError = function(error) {
2887 self.table_.list.endBatchUpdates();
2888 self.grid_.endBatchUpdates();
2890 self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
2891 util.getFileErrorString(error.name)));
2894 var onAbort = function() {
2895 self.table_.list.endBatchUpdates();
2896 self.grid_.endBatchUpdates();
2899 this.table_.list.startBatchUpdates();
2900 this.grid_.startBatchUpdates();
2901 this.directoryModel_.createDirectory(current(),
2908 * Handles click event on the toggle-view button.
2909 * @param {Event} event Click event.
2912 FileManager.prototype.onToggleViewButtonClick_ = function(event) {
2913 if (this.listType_ === FileManager.ListType.DETAIL)
2914 this.setListType(FileManager.ListType.THUMBNAIL);
2916 this.setListType(FileManager.ListType.DETAIL);
2918 event.target.blur();
2922 * KeyDown event handler for the document.
2923 * @param {Event} event Key event.
2926 FileManager.prototype.onKeyDown_ = function(event) {
2927 if (event.keyCode === 9) // Tab
2928 this.pressingTab_ = true;
2929 if (event.keyCode === 17) // Ctrl
2930 this.pressingCtrl_ = true;
2932 if (event.srcElement === this.renameInput_) {
2933 // Ignore keydown handler in the rename input box.
2937 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
2938 case 'Ctrl-U+00BE': // Ctrl-. => Toggle filter files.
2939 this.fileFilter_.setFilterHidden(
2940 !this.fileFilter_.isFilterHiddenOn());
2941 event.preventDefault();
2944 case 'U+001B': // Escape => Cancel dialog.
2945 if (this.dialogType != DialogType.FULL_PAGE) {
2946 // If there is nothing else for ESC to do, then cancel the dialog.
2947 event.preventDefault();
2948 this.cancelButton_.click();
2955 * KeyUp event handler for the document.
2956 * @param {Event} event Key event.
2959 FileManager.prototype.onKeyUp_ = function(event) {
2960 if (event.keyCode === 9) // Tab
2961 this.pressingTab_ = false;
2962 if (event.keyCode == 17) // Ctrl
2963 this.pressingCtrl_ = false;
2967 * KeyDown event handler for the div#list-container element.
2968 * @param {Event} event Key event.
2971 FileManager.prototype.onListKeyDown_ = function(event) {
2972 if (event.srcElement.tagName == 'INPUT') {
2973 // Ignore keydown handler in the rename input box.
2977 switch (util.getKeyModifiers(event) + event.keyCode) {
2978 case '8': // Backspace => Up one directory.
2979 event.preventDefault();
2980 // TODO(mtomasz): Use Entry.getParent() instead.
2981 if (!this.getCurrentDirectoryEntry())
2983 var currentEntry = this.getCurrentDirectoryEntry();
2984 var locationInfo = this.volumeManager_.getLocationInfo(currentEntry);
2985 // TODO(mtomasz): There may be a tiny race in here.
2986 if (locationInfo && !locationInfo.isRootEntry &&
2987 !locationInfo.isSpecialSearchRoot) {
2988 currentEntry.getParent(function(parentEntry) {
2989 this.directoryModel_.changeDirectoryEntry(parentEntry);
2990 }.bind(this), function() { /* Ignore errors. */});
2994 case '13': // Enter => Change directory or perform default action.
2995 // TODO(dgozman): move directory action to dispatchSelectionAction.
2996 var selection = this.getSelection();
2997 if (selection.totalCount == 1 &&
2998 selection.entries[0].isDirectory &&
2999 !DialogType.isFolderDialog(this.dialogType)) {
3000 event.preventDefault();
3001 this.onDirectoryAction_(selection.entries[0]);
3002 } else if (this.dispatchSelectionAction_()) {
3003 event.preventDefault();
3008 switch (event.keyIdentifier) {
3015 // When navigating with keyboard we hide the distracting mouse hover
3016 // highlighting until the user moves the mouse again.
3017 this.setNoHover_(true);
3023 * Suppress/restore hover highlighting in the list container.
3024 * @param {boolean} on True to temporarity hide hover state.
3027 FileManager.prototype.setNoHover_ = function(on) {
3029 this.listContainer_.classList.add('nohover');
3031 this.listContainer_.classList.remove('nohover');
3036 * KeyPress event handler for the div#list-container element.
3037 * @param {Event} event Key event.
3040 FileManager.prototype.onListKeyPress_ = function(event) {
3041 if (event.srcElement.tagName == 'INPUT') {
3042 // Ignore keypress handler in the rename input box.
3046 if (event.ctrlKey || event.metaKey || event.altKey)
3049 var now = new Date();
3050 var char = String.fromCharCode(event.charCode).toLowerCase();
3051 var text = now - this.textSearchState_.date > 1000 ? '' :
3052 this.textSearchState_.text;
3053 this.textSearchState_ = {text: text + char, date: now};
3055 this.doTextSearch_();
3059 * Mousemove event handler for the div#list-container element.
3060 * @param {Event} event Mouse event.
3063 FileManager.prototype.onListMouseMove_ = function(event) {
3064 // The user grabbed the mouse, restore the hover highlighting.
3065 this.setNoHover_(false);
3069 * Performs a 'text search' - selects a first list entry with name
3070 * starting with entered text (case-insensitive).
3073 FileManager.prototype.doTextSearch_ = function() {
3074 var text = this.textSearchState_.text;
3078 var dm = this.directoryModel_.getFileList();
3079 for (var index = 0; index < dm.length; ++index) {
3080 var name = dm.item(index).name;
3081 if (name.substring(0, text.length).toLowerCase() == text) {
3082 this.currentList_.selectionModel.selectedIndexes = [index];
3087 this.textSearchState_.text = '';
3091 * Handle a click of the cancel button. Closes the window.
3092 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3094 * @param {Event} event The click event.
3097 FileManager.prototype.onCancel_ = function(event) {
3098 chrome.fileBrowserPrivate.cancelDialog();
3103 * Resolves selected file urls returned from an Open dialog.
3105 * For drive files this involves some special treatment.
3106 * Starts getting drive files if needed.
3108 * @param {Array.<string>} fileUrls Drive URLs.
3109 * @param {function(Array.<string>)} callback To be called with fixed URLs.
3112 FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) {
3113 if (this.isOnDrive()) {
3114 chrome.fileBrowserPrivate.getDriveFiles(
3116 function(localPaths) {
3125 * Closes this modal dialog with some files selected.
3126 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3127 * @param {Object} selection Contains urls, filterIndex and multiple fields.
3130 FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) {
3132 function callback() {
3135 if (selection.multiple) {
3136 chrome.fileBrowserPrivate.selectFiles(
3137 selection.urls, this.params_.shouldReturnLocalPath, callback);
3139 var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE);
3140 chrome.fileBrowserPrivate.selectFile(
3141 selection.urls[0], selection.filterIndex, forOpening,
3142 this.params_.shouldReturnLocalPath, callback);
3147 * Tries to close this modal dialog with some files selected.
3148 * Performs preprocessing if needed (e.g. for Drive).
3149 * @param {Object} selection Contains urls, filterIndex and multiple fields.
3152 FileManager.prototype.selectFilesAndClose_ = function(selection) {
3153 if (!this.isOnDrive() ||
3154 this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3155 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3159 var shade = this.document_.createElement('div');
3160 shade.className = 'shade';
3161 var footer = this.dialogDom_.querySelector('.button-panel');
3162 var progress = footer.querySelector('.progress-track');
3163 progress.style.width = '0%';
3164 var cancelled = false;
3166 var progressMap = {};
3167 var filesStarted = 0;
3168 var filesTotal = selection.urls.length;
3169 for (var index = 0; index < selection.urls.length; index++) {
3170 progressMap[selection.urls[index]] = -1;
3172 var lastPercent = 0;
3176 var onFileTransfersUpdated = function(statusList) {
3177 for (var index = 0; index < statusList.length; index++) {
3178 var status = statusList[index];
3179 var escaped = encodeURI(status.fileUrl);
3180 if (!(escaped in progressMap)) continue;
3181 if (status.total == -1) continue;
3183 var old = progressMap[escaped];
3185 // -1 means we don't know file size yet.
3186 bytesTotal += status.total;
3190 bytesDone += status.processed - old;
3191 progressMap[escaped] = status.processed;
3194 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
3195 // For files we don't have information about, assume the progress is zero.
3196 percent = percent * filesStarted / filesTotal * 100;
3197 // Do not decrease the progress. This may happen, if first downloaded
3198 // file is small, and the second one is large.
3199 lastPercent = Math.max(lastPercent, percent);
3200 progress.style.width = lastPercent + '%';
3203 var setup = function() {
3204 this.document_.querySelector('.dialog-container').appendChild(shade);
3205 setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100);
3206 footer.setAttribute('progress', 'progress');
3207 this.cancelButton_.removeEventListener('click', this.onCancelBound_);
3208 this.cancelButton_.addEventListener('click', onCancel);
3209 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
3210 onFileTransfersUpdated);
3213 var cleanup = function() {
3214 shade.parentNode.removeChild(shade);
3215 footer.removeAttribute('progress');
3216 this.cancelButton_.removeEventListener('click', onCancel);
3217 this.cancelButton_.addEventListener('click', this.onCancelBound_);
3218 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
3219 onFileTransfersUpdated);
3222 var onCancel = function() {
3224 // According to API cancel may fail, but there is no proper UI to reflect
3225 // this. So, we just silently assume that everything is cancelled.
3226 chrome.fileBrowserPrivate.cancelFileTransfers(
3227 selection.urls, function(response) {});
3231 var onResolved = function(resolvedUrls) {
3232 if (cancelled) return;
3234 selection.urls = resolvedUrls;
3235 // Call next method on a timeout, as it's unsafe to
3236 // close a window from a callback.
3237 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3240 var onProperties = function(properties) {
3241 for (var i = 0; i < properties.length; i++) {
3242 if (!properties[i] || properties[i].present) {
3243 // For files already in GCache, we don't get any transfer updates.
3247 this.resolveSelectResults_(selection.urls, onResolved);
3252 // TODO(mtomasz): Use Entry instead of URLs, if possible.
3253 util.URLsToEntries(selection.urls, function(entries) {
3254 this.metadataCache_.get(entries, 'drive', onProperties);
3259 * Handle a click of the ok button.
3261 * The ok button has different UI labels depending on the type of dialog, but
3262 * in code it's always referred to as 'ok'.
3264 * @param {Event} event The click event.
3267 FileManager.prototype.onOk_ = function(event) {
3268 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3269 // Save-as doesn't require a valid selection from the list, since
3270 // we're going to take the filename from the text input.
3271 var filename = this.filenameInput_.value;
3273 throw new Error('Missing filename!');
3275 var directory = this.getCurrentDirectoryEntry();
3276 this.validateFileName_(directory, filename, function(isValid) {
3280 if (util.isFakeEntry(directory)) {
3281 // Can't save a file into a fake directory.
3285 var selectFileAndClose = function() {
3286 // TODO(mtomasz): Clean this up by avoiding constructing a URL
3287 // via string concatenation.
3288 var currentDirUrl = directory.toURL();
3289 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
3290 currentDirUrl += '/';
3291 this.selectFilesAndClose_({
3292 urls: [currentDirUrl + encodeURIComponent(filename)],
3294 filterIndex: this.getSelectedFilterIndex_(filename)
3299 filename, {create: false},
3301 // An existing file is found. Show confirmation dialog to
3302 // overwrite it. If the user select "OK" on the dialog, save it.
3303 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
3304 selectFileAndClose);
3307 if (error.name == util.FileError.NOT_FOUND_ERR) {
3308 // The file does not exist, so it should be ok to create a
3310 selectFileAndClose();
3313 if (error.name == util.FileError.TYPE_MISMATCH_ERR) {
3314 // An directory is found.
3315 // Do not allow to overwrite directory.
3316 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
3320 // Unexpected error.
3321 console.error('File save failed: ' + error.code);
3328 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
3330 if (DialogType.isFolderDialog(this.dialogType) &&
3331 selectedIndexes.length == 0) {
3332 var url = this.getCurrentDirectoryEntry().toURL();
3333 var singleSelection = {
3336 filterIndex: this.getSelectedFilterIndex_()
3338 this.selectFilesAndClose_(singleSelection);
3342 // All other dialog types require at least one selected list item.
3343 // The logic to control whether or not the ok button is enabled should
3344 // prevent us from ever getting here, but we sanity check to be sure.
3345 if (!selectedIndexes.length)
3346 throw new Error('Nothing selected!');
3348 var dm = this.directoryModel_.getFileList();
3349 for (var i = 0; i < selectedIndexes.length; i++) {
3350 var entry = dm.item(selectedIndexes[i]);
3352 console.error('Error locating selected file at index: ' + i);
3356 files.push(entry.toURL());
3359 // Multi-file selection has no other restrictions.
3360 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
3361 var multipleSelection = {
3365 this.selectFilesAndClose_(multipleSelection);
3369 // Everything else must have exactly one.
3370 if (files.length > 1)
3371 throw new Error('Too many files selected!');
3373 var selectedEntry = dm.item(selectedIndexes[0]);
3375 if (DialogType.isFolderDialog(this.dialogType)) {
3376 if (!selectedEntry.isDirectory)
3377 throw new Error('Selected entry is not a folder!');
3378 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
3379 if (!selectedEntry.isFile)
3380 throw new Error('Selected entry is not a file!');
3383 var singleSelection = {
3386 filterIndex: this.getSelectedFilterIndex_()
3388 this.selectFilesAndClose_(singleSelection);
3392 * Verifies the user entered name for file or folder to be created or
3393 * renamed to. See also util.validateFileName.
3395 * @param {DirectoryEntry} parentEntry The URL of the parent directory entry.
3396 * @param {string} name New file or folder name.
3397 * @param {function} onDone Function to invoke when user closes the
3398 * warning box or immediatelly if file name is correct. If the name was
3399 * valid it is passed true, and false otherwise.
3402 FileManager.prototype.validateFileName_ = function(
3403 parentEntry, name, onDone) {
3404 var fileNameErrorPromise = util.validateFileName(
3407 this.fileFilter_.isFilterHiddenOn());
3408 fileNameErrorPromise.then(
3409 onDone.bind(null, true),
3411 this.alert.show(message, onDone.bind(null, false));
3412 }.bind(this)).catch(function(error) {
3413 console.error(error.stack || error);
3418 * Toggle whether mobile data is used for sync.
3420 FileManager.prototype.toggleDriveSyncSettings = function() {
3421 // If checked, the sync is disabled.
3422 var nowCellularDisabled = this.syncButton.hasAttribute('checked');
3423 var changeInfo = {cellularDisabled: !nowCellularDisabled};
3424 chrome.fileBrowserPrivate.setPreferences(changeInfo);
3428 * Toggle whether Google Docs files are shown.
3430 FileManager.prototype.toggleDriveHostedSettings = function() {
3431 // If checked, showing drive hosted files is enabled.
3432 var nowHostedFilesEnabled = this.hostedButton.hasAttribute('checked');
3433 var nowHostedFilesDisabled = !nowHostedFilesEnabled;
3435 var changeInfo = {hostedFilesDisabled: !nowHostedFilesDisabled};
3437 var changeInfo = {};
3438 changeInfo['hostedFilesDisabled'] = !nowHostedFilesDisabled;
3439 chrome.fileBrowserPrivate.setPreferences(changeInfo);
3443 * Invoked when the search box is changed.
3445 * @param {Event} event The changed event.
3448 FileManager.prototype.onSearchBoxUpdate_ = function(event) {
3449 var searchString = this.searchBox_.value;
3451 if (this.isOnDrive()) {
3452 // When the search text is changed, finishes the search and showes back
3453 // the last directory by passing an empty string to
3454 // {@code DirectoryModel.search()}.
3455 if (this.directoryModel_.isSearching() &&
3456 this.lastSearchQuery_ != searchString) {
3460 // On drive, incremental search is not invoked since we have an auto-
3461 // complete suggestion instead.
3465 this.search_(searchString);
3469 * Handle the search clear button click.
3472 FileManager.prototype.onSearchClearButtonClick_ = function() {
3473 this.ui_.searchBox.clear();
3474 this.onSearchBoxUpdate_();
3478 * Search files and update the list with the search result.
3480 * @param {string} searchString String to be searched with.
3483 FileManager.prototype.search_ = function(searchString) {
3484 var noResultsDiv = this.document_.getElementById('no-search-results');
3486 var reportEmptySearchResults = function() {
3487 if (this.directoryModel_.getFileList().length === 0) {
3488 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
3489 // hence we escapes |searchString| here.
3490 var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
3491 util.htmlEscape(searchString));
3492 noResultsDiv.innerHTML = html;
3493 noResultsDiv.setAttribute('show', 'true');
3495 noResultsDiv.removeAttribute('show');
3498 // If the current location is somewhere in Drive, all files in Drive can
3499 // be listed as search results regardless of current location.
3500 // In this case, showing current location is confusing, so use the Drive
3501 // root "My Drive" as the current location.
3502 var entry = this.getCurrentDirectoryEntry();
3503 var locationInfo = this.volumeManager_.getLocationInfo(entry);
3504 if (locationInfo && locationInfo.isDriveBased) {
3505 var rootEntry = locationInfo.volumeInfo.displayRoot;
3507 this.updateLocationLine_(rootEntry);
3511 var hideNoResultsDiv = function() {
3512 noResultsDiv.removeAttribute('show');
3513 this.updateLocationLine_();
3516 this.doSearch(searchString,
3517 reportEmptySearchResults.bind(this),
3518 hideNoResultsDiv.bind(this));
3522 * Performs search and displays results.
3524 * @param {string} query Query that will be searched for.
3525 * @param {function()=} opt_onSearchRescan Function that will be called when
3526 * the search directory is rescanned (i.e. search results are displayed).
3527 * @param {function()=} opt_onClearSearch Function to be called when search
3528 * state gets cleared.
3530 FileManager.prototype.doSearch = function(
3531 searchString, opt_onSearchRescan, opt_onClearSearch) {
3532 var onSearchRescan = opt_onSearchRescan || function() {};
3533 var onClearSearch = opt_onClearSearch || function() {};
3535 this.lastSearchQuery_ = searchString;
3536 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
3540 * Requests autocomplete suggestions for files on Drive.
3541 * Once the suggestions are returned, the autocomplete popup will show up.
3543 * @param {string} query The text to autocomplete from.
3546 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
3547 query = query.trimLeft();
3549 // Only Drive supports auto-compelete
3550 if (!this.isOnDrive())
3553 // Remember the most recent query. If there is an other request in progress,
3554 // then it's result will be discarded and it will call a new request for
3556 this.lastAutocompleteQuery_ = query;
3557 if (this.autocompleteSuggestionsBusy_)
3560 // The autocomplete list should be resized and repositioned here as the
3561 // search box is resized when it's focused.
3562 this.autocompleteList_.syncWidthAndPositionToInput();
3565 this.autocompleteList_.suggestions = [];
3569 var headerItem = {isHeaderItem: true, searchQuery: query};
3570 if (!this.autocompleteList_.dataModel ||
3571 this.autocompleteList_.dataModel.length == 0)
3572 this.autocompleteList_.suggestions = [headerItem];
3574 // Updates only the head item to prevent a flickering on typing.
3575 this.autocompleteList_.dataModel.splice(0, 1, headerItem);
3577 this.autocompleteSuggestionsBusy_ = true;
3579 var searchParams = {
3584 chrome.fileBrowserPrivate.searchDriveMetadata(
3586 function(suggestions) {
3587 this.autocompleteSuggestionsBusy_ = false;
3589 // Discard results for previous requests and fire a new search
3590 // for the most recent query.
3591 if (query != this.lastAutocompleteQuery_) {
3592 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
3596 // Keeps the items in the suggestion list.
3597 this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
3602 * Opens the currently selected suggestion item.
3605 FileManager.prototype.openAutocompleteSuggestion_ = function() {
3606 var selectedItem = this.autocompleteList_.selectedItem;
3608 // If the entry is the search item or no entry is selected, just change to
3609 // the search result.
3610 if (!selectedItem || selectedItem.isHeaderItem) {
3611 var query = selectedItem ?
3612 selectedItem.searchQuery : this.searchBox_.value;
3613 this.search_(query);
3617 var entry = selectedItem.entry;
3618 // If the entry is a directory, just change the directory.
3619 if (entry.isDirectory) {
3620 this.onDirectoryAction_(entry);
3624 var entries = [entry];
3627 // To open a file, first get the mime type.
3628 this.metadataCache_.get(entries, 'drive', function(props) {
3629 var mimeType = props[0].contentMimeType || '';
3630 var mimeTypes = [mimeType];
3631 var openIt = function() {
3632 if (self.dialogType == DialogType.FULL_PAGE) {
3633 var tasks = new FileTasks(self);
3634 tasks.init(entries, mimeTypes);
3635 tasks.executeDefault();
3641 // Change the current directory to the directory that contains the
3642 // selected file. Note that this is necessary for an image or a video,
3643 // which should be opened in the gallery mode, as the gallery mode
3644 // requires the entry to be in the current directory model. For
3645 // consistency, the current directory is always changed regardless of
3647 entry.getParent(function(parentEntry) {
3648 var onDirectoryChanged = function(event) {
3649 self.directoryModel_.removeEventListener('scan-completed',
3650 onDirectoryChanged);
3651 self.directoryModel_.selectEntry(entry);
3654 // changeDirectoryEntry() returns immediately. We should wait until the
3655 // directory scan is complete.
3656 self.directoryModel_.addEventListener('scan-completed',
3657 onDirectoryChanged);
3658 self.directoryModel_.changeDirectoryEntry(
3661 // Remove the listner if the change directory failed.
3662 self.directoryModel_.removeEventListener('scan-completed',
3663 onDirectoryChanged);
3669 FileManager.prototype.decorateSplitter = function(splitterElement) {
3672 var Splitter = cr.ui.Splitter;
3674 var customSplitter = cr.ui.define('div');
3676 customSplitter.prototype = {
3677 __proto__: Splitter.prototype,
3679 handleSplitterDragStart: function(e) {
3680 Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
3681 this.ownerDocument.documentElement.classList.add('col-resize');
3684 handleSplitterDragMove: function(deltaX) {
3685 Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
3689 handleSplitterDragEnd: function(e) {
3690 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
3691 this.ownerDocument.documentElement.classList.remove('col-resize');
3695 customSplitter.decorate(splitterElement);
3699 * Updates default action menu item to match passed taskItem (icon,
3700 * label and action).
3702 * @param {Object} defaultItem - taskItem to match.
3703 * @param {boolean} isMultiple - if multiple tasks available.
3705 FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
3708 if (defaultItem.iconType) {
3709 this.defaultActionMenuItem_.style.backgroundImage = '';
3710 this.defaultActionMenuItem_.setAttribute('file-type-icon',
3711 defaultItem.iconType);
3712 } else if (defaultItem.iconUrl) {
3713 this.defaultActionMenuItem_.style.backgroundImage =
3714 'url(' + defaultItem.iconUrl + ')';
3716 this.defaultActionMenuItem_.style.backgroundImage = '';
3719 this.defaultActionMenuItem_.label = defaultItem.title;
3720 this.defaultActionMenuItem_.disabled = !!defaultItem.disabled;
3721 this.defaultActionMenuItem_.taskId = defaultItem.taskId;
3724 var defaultActionSeparator =
3725 this.dialogDom_.querySelector('#default-action-separator');
3727 this.openWithCommand_.canExecuteChange();
3728 this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
3729 this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled;
3731 this.defaultActionMenuItem_.hidden = !defaultItem;
3732 defaultActionSeparator.hidden = !defaultItem;
3736 * @return {FileSelection} Selection object.
3738 FileManager.prototype.getSelection = function() {
3739 return this.selectionHandler_.selection;
3743 * @return {ArrayDataModel} File list.
3745 FileManager.prototype.getFileList = function() {
3746 return this.directoryModel_.getFileList();
3750 * @return {cr.ui.List} Current list object.
3752 FileManager.prototype.getCurrentList = function() {
3753 return this.currentList_;
3757 * Retrieve the preferences of the files.app. This method caches the result
3758 * and returns it unless opt_update is true.
3759 * @param {function(Object.<string, *>)} callback Callback to get the
3761 * @param {boolean=} opt_update If is's true, don't use the cache and
3762 * retrieve latest preference. Default is false.
3765 FileManager.prototype.getPreferences_ = function(callback, opt_update) {
3766 if (!opt_update && this.preferences_ !== undefined) {
3767 callback(this.preferences_);
3771 chrome.fileBrowserPrivate.getPreferences(function(prefs) {
3772 this.preferences_ = prefs;