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 * shoudl 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_;
77 get navigationList() {
78 return this.navigationList_;
81 return this.document_;
83 get fileTransferController() {
84 return this.fileTransferController_;
86 get backgroundPage() {
87 return this.backgroundPage_;
90 return this.volumeManager_;
98 * List of dialog types.
100 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
101 * FULL_PAGE which is specific to this code.
106 SELECT_FOLDER: 'folder',
107 SELECT_UPLOAD_FOLDER: 'upload-folder',
108 SELECT_SAVEAS_FILE: 'saveas-file',
109 SELECT_OPEN_FILE: 'open-file',
110 SELECT_OPEN_MULTI_FILE: 'open-multi-file',
111 FULL_PAGE: 'full-page'
115 * @param {string} type Dialog type.
116 * @return {boolean} Whether the type is modal.
118 DialogType.isModal = function(type) {
119 return type == DialogType.SELECT_FOLDER ||
120 type == DialogType.SELECT_UPLOAD_FOLDER ||
121 type == DialogType.SELECT_SAVEAS_FILE ||
122 type == DialogType.SELECT_OPEN_FILE ||
123 type == DialogType.SELECT_OPEN_MULTI_FILE;
127 * @param {string} type Dialog type.
128 * @return {boolean} Whether the type is open dialog.
130 DialogType.isOpenDialog = function(type) {
131 return type == DialogType.SELECT_OPEN_FILE ||
132 type == DialogType.SELECT_OPEN_MULTI_FILE ||
133 type == DialogType.SELECT_FOLDER ||
134 type == DialogType.SELECT_UPLOAD_FOLDER;
138 * @param {string} type Dialog type.
139 * @return {boolean} Whether the type is folder selection dialog.
141 DialogType.isFolderDialog = function(type) {
142 return type == DialogType.SELECT_FOLDER ||
143 type == DialogType.SELECT_UPLOAD_FOLDER;
147 * Bottom margin of the list and tree for transparent preview panel.
150 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
152 // Anonymous "namespace".
155 // Private variables and helper functions.
158 * Number of milliseconds in a day.
160 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
163 * Some UI elements react on a single click and standard double click handling
164 * leads to confusing results. We ignore a second click if it comes soon
167 var DOUBLE_CLICK_TIMEOUT = 200;
170 * Update the element to display the information about remaining space for
172 * @param {!Element} spaceInnerBar Block element for a percentage bar
173 * representing the remaining space.
174 * @param {!Element} spaceInfoLabel Inline element to contain the message.
175 * @param {!Element} spaceOuterBar Block element around the percentage bar.
177 var updateSpaceInfo = function(
178 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
179 spaceInnerBar.removeAttribute('pending');
180 if (sizeStatsResult) {
181 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
182 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
185 sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
186 spaceInnerBar.style.width =
187 (100 * usedSpace / sizeStatsResult.totalSize) + '%';
189 spaceOuterBar.hidden = false;
191 spaceOuterBar.hidden = true;
192 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
198 FileManager.ListType = {
203 FileManager.prototype.initPreferences_ = function(callback) {
204 var group = new AsyncUtil.Group();
206 // DRIVE preferences should be initialized before creating DirectoryModel
207 // to rebuild the roots list.
208 group.add(this.getPreferences_.bind(this));
210 // Get startup preferences.
211 this.viewOptions_ = {};
212 group.add(function(done) {
213 util.platform.getPreference(this.startupPrefName_, function(value) {
214 // Load the global default options.
216 this.viewOptions_ = JSON.parse(value);
218 // Override with window-specific options.
219 if (window.appState && window.appState.viewOptions) {
220 for (var key in window.appState.viewOptions) {
221 if (window.appState.viewOptions.hasOwnProperty(key))
222 this.viewOptions_[key] = window.appState.viewOptions[key];
233 * One time initialization for the file system and related things.
235 * @param {function()} callback Completion callback.
238 FileManager.prototype.initFileSystemUI_ = function(callback) {
239 this.table_.startBatchUpdates();
240 this.grid_.startBatchUpdates();
242 this.initFileList_();
243 this.setupCurrentDirectory_();
245 // PyAuto tests monitor this state by polling this variable
246 this.__defineGetter__('workerInitialized_', function() {
247 return this.metadataCache_.isInitialized();
250 this.initDateTimeFormatters_();
254 // Get the 'allowRedeemOffers' preference before launching
255 // FileListBannerController.
256 this.getPreferences_(function(pref) {
257 /** @type {boolean} */
258 var showOffers = pref['allowRedeemOffers'];
259 self.bannersController_ = new FileListBannerController(
260 self.directoryModel_, self.volumeManager_, self.document_,
262 self.bannersController_.addEventListener('relayout',
263 self.onResize_.bind(self));
266 var dm = this.directoryModel_;
267 dm.addEventListener('directory-changed',
268 this.onDirectoryChanged_.bind(this));
269 dm.addEventListener('begin-update-files', function() {
270 self.currentList_.startBatchUpdates();
272 dm.addEventListener('end-update-files', function() {
273 self.restoreItemBeingRenamed_();
274 self.currentList_.endBatchUpdates();
276 dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
277 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
278 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
279 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
280 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
281 dm.addEventListener('rescan-completed',
282 this.onRescanCompleted_.bind(this));
284 this.directoryTree_.addEventListener('change', function() {
285 this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
288 var stateChangeHandler =
289 this.onPreferencesChanged_.bind(this);
290 chrome.fileBrowserPrivate.onPreferencesChanged.addListener(
292 stateChangeHandler();
294 var driveConnectionChangedHandler =
295 this.onDriveConnectionChanged_.bind(this);
296 this.volumeManager_.addEventListener('drive-connection-changed',
297 driveConnectionChangedHandler);
298 driveConnectionChangedHandler();
300 // Set the initial focus.
302 // Set it as a fallback when there is no focus.
303 this.document_.addEventListener('focusout', function(e) {
304 setTimeout(function() {
305 // When there is no focus, the active element is the <body>.
306 if (this.document_.activeElement == this.document_.body)
311 this.initDataTransferOperations_();
313 this.initContextMenus_();
314 this.initCommands_();
316 this.updateFileTypeFilter_();
318 this.selectionHandler_.onFileSelectionChanged();
320 this.table_.endBatchUpdates();
321 this.grid_.endBatchUpdates();
327 * If |item| in the directory tree is behind the preview panel, scrolls up the
328 * parent view and make the item visible. This should be called when:
329 * - the selected item is changed in the directory tree.
330 * - the visibility of the the preview panel is changed.
334 FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ =
336 var selectedSubTree = this.directoryTree_.selectedItem;
337 if (!selectedSubTree)
339 var item = selectedSubTree.rowElement;
340 var parentView = this.directoryTree_;
342 var itemRect = item.getBoundingClientRect();
346 var listRect = parentView.getBoundingClientRect();
350 var previewPanel = this.dialogDom_.querySelector('.preview-panel');
351 var previewPanelRect = previewPanel.getBoundingClientRect();
352 var panelHeight = previewPanelRect ? previewPanelRect.height : 0;
354 var itemBottom = itemRect.bottom;
355 var listBottom = listRect.bottom - panelHeight;
357 if (itemBottom > listBottom) {
358 var scrollOffset = itemBottom - listBottom;
359 parentView.scrollTop += scrollOffset;
366 FileManager.prototype.initDateTimeFormatters_ = function() {
367 var use12hourClock = !this.preferences_['use24hourClock'];
368 this.table_.setDateTimeFormat(use12hourClock);
374 FileManager.prototype.initDataTransferOperations_ = function() {
375 this.fileOperationManager_ =
376 this.backgroundPage_.background.fileOperationManager;
378 // CopyManager are required for 'Delete' operation in
379 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
380 if (this.dialogType != DialogType.FULL_PAGE) return;
382 // TODO(hidehiko): Extract FileOperationManager related code from
383 // FileManager to simplify it.
384 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
385 this.fileOperationManager_.addEventListener(
386 'copy-progress', this.onCopyProgressBound_);
388 this.onEntryChangedBound_ = this.onEntryChanged_.bind(this);
389 this.fileOperationManager_.addEventListener(
390 'entry-changed', this.onEntryChangedBound_);
392 var controller = this.fileTransferController_ =
393 new FileTransferController(this.document_,
394 this.fileOperationManager_,
396 this.directoryModel_,
398 this.ui_.multiProfileShareDialog);
399 controller.attachDragSource(this.table_.list);
400 controller.attachFileListDropTarget(this.table_.list);
401 controller.attachDragSource(this.grid_);
402 controller.attachFileListDropTarget(this.grid_);
403 controller.attachTreeDropTarget(this.directoryTree_);
404 controller.attachNavigationListDropTarget(this.navigationList_, true);
405 controller.attachCopyPasteHandlers();
406 controller.addEventListener('selection-copied',
407 this.blinkSelection.bind(this));
408 controller.addEventListener('selection-cut',
409 this.blinkSelection.bind(this));
410 controller.addEventListener('source-not-found',
411 this.onSourceNotFound_.bind(this));
415 * Handles an error that the source entry of file operation is not found.
418 FileManager.prototype.onSourceNotFound_ = function(event) {
419 // Ensure this.sourceNotFoundErrorCount_ is integer.
420 this.sourceNotFoundErrorCount_ = ~~this.sourceNotFoundErrorCount_;
421 var item = new ProgressCenterItem();
422 item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_;
423 if (event.progressType === ProgressItemType.COPY)
424 item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', event.fileName);
425 else if (event.progressType === ProgressItemType.MOVE)
426 item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', event.fileName);
427 item.state = ProgressItemState.ERROR;
428 this.backgroundPage_.background.progressCenter.updateItem(item);
429 this.sourceNotFoundErrorCount_++;
433 * One-time initialization of context menus.
436 FileManager.prototype.initContextMenus_ = function() {
437 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
438 cr.ui.Menu.decorate(this.fileContextMenu_);
440 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
441 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
442 this.fileContextMenu_);
443 cr.ui.contextMenuHandler.setContextMenu(
444 this.document_.querySelector('.drive-welcome.page'),
445 this.fileContextMenu_);
447 this.rootsContextMenu_ =
448 this.dialogDom_.querySelector('#roots-context-menu');
449 cr.ui.Menu.decorate(this.rootsContextMenu_);
450 this.navigationList_.setContextMenu(this.rootsContextMenu_);
452 this.directoryTreeContextMenu_ =
453 this.dialogDom_.querySelector('#directory-tree-context-menu');
454 cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
455 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
457 this.textContextMenu_ =
458 this.dialogDom_.querySelector('#text-context-menu');
459 cr.ui.Menu.decorate(this.textContextMenu_);
461 this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
462 this.gearButton_.addEventListener('menushow',
463 this.onShowGearMenu_.bind(this));
464 chrome.fileBrowserPrivate.onDesktopChanged.addListener(function() {
465 this.updateVisitDesktopMenus_();
466 this.ui_.updateProfileBadge();
468 chrome.fileBrowserPrivate.onProfileAdded.addListener(
469 this.updateVisitDesktopMenus_.bind(this));
470 this.updateVisitDesktopMenus_();
472 this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
474 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
476 if (this.dialogType == DialogType.FULL_PAGE) {
477 // This is to prevent the buttons from stealing focus on mouse down.
478 var preventFocus = function(event) {
479 event.preventDefault();
482 var minimizeButton = this.dialogDom_.querySelector('#minimize-button');
483 minimizeButton.addEventListener('click', this.onMinimize.bind(this));
484 minimizeButton.addEventListener('mousedown', preventFocus);
486 var maximizeButton = this.dialogDom_.querySelector('#maximize-button');
487 maximizeButton.addEventListener('click', this.onMaximize.bind(this));
488 maximizeButton.addEventListener('mousedown', preventFocus);
490 var closeButton = this.dialogDom_.querySelector('#close-button');
491 closeButton.addEventListener('click', this.onClose.bind(this));
492 closeButton.addEventListener('mousedown', preventFocus);
495 this.syncButton.checkable = true;
496 this.hostedButton.checkable = true;
497 this.detailViewButton_.checkable = true;
498 this.thumbnailViewButton_.checkable = true;
500 if (util.platform.runningInBrowser()) {
501 // Suppresses the default context menu.
502 this.dialogDom_.addEventListener('contextmenu', function(e) {
509 FileManager.prototype.onMinimize = function() {
510 chrome.app.window.current().minimize();
513 FileManager.prototype.onMaximize = function() {
514 var appWindow = chrome.app.window.current();
515 if (appWindow.isMaximized())
518 appWindow.maximize();
521 FileManager.prototype.onClose = function() {
525 FileManager.prototype.onShowGearMenu_ = function() {
526 this.refreshRemainingSpace_(false); /* Without loading caption. */
528 // If the menu is opened while CTRL key pressed, secret menu itemscan be
530 this.isSecretGearMenuShown_ = this.pressingCtrl_;
532 // Update view of drive-related settings.
533 this.commandHandler.updateAvailability();
534 this.document_.getElementById('drive-separator').hidden =
535 !this.shouldShowDriveSettings();
537 // Force to update the gear menu position.
538 // TODO(hirono): Remove the workaround for the crbug.com/374093 after fixing
540 var gearMenu = this.document_.querySelector('#gear-menu');
541 gearMenu.style.left = '';
542 gearMenu.style.right = '';
543 gearMenu.style.top = '';
544 gearMenu.style.bottom = '';
548 * One-time initialization of commands.
551 FileManager.prototype.initCommands_ = function() {
552 this.commandHandler = new CommandHandler(this);
554 // TODO(hirono): Move the following block to the UI part.
555 var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
556 for (var j = 0; j < commandButtons.length; j++)
557 CommandButton.decorate(commandButtons[j]);
559 var inputs = this.dialogDom_.querySelectorAll(
560 'input[type=text], input[type=search], textarea');
561 for (var i = 0; i < inputs.length; i++) {
562 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
563 this.registerInputCommands_(inputs[i]);
566 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
567 this.textContextMenu_);
568 this.registerInputCommands_(this.renameInput_);
569 this.document_.addEventListener('command',
570 this.setNoHover_.bind(this, true));
574 * Registers cut, copy, paste and delete commands on input element.
576 * @param {Node} node Text input element to register on.
579 FileManager.prototype.registerInputCommands_ = function(node) {
580 CommandUtil.forceDefaultHandler(node, 'cut');
581 CommandUtil.forceDefaultHandler(node, 'copy');
582 CommandUtil.forceDefaultHandler(node, 'paste');
583 CommandUtil.forceDefaultHandler(node, 'delete');
584 node.addEventListener('keydown', function(e) {
585 var key = util.getKeyModifiers(e) + e.keyCode;
586 if (key === '190' /* '/' */ || key === '191' /* '.' */) {
587 // If this key event is propagated, this is handled search command,
588 // which calls 'preventDefault' method.
595 * Entry point of the initialization.
596 * This method is called from main.js.
598 FileManager.prototype.initializeCore = function() {
599 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
600 this.initializeQueue_.add(this.initBackgroundPage_.bind(this),
601 [], 'initBackgroundPage');
602 this.initializeQueue_.add(this.initPreferences_.bind(this),
603 ['initGeneral'], 'initPreferences');
604 this.initializeQueue_.add(this.initVolumeManager_.bind(this),
605 ['initGeneral', 'initBackgroundPage'],
606 'initVolumeManager');
608 this.initializeQueue_.run();
609 window.addEventListener('pagehide', this.onUnload_.bind(this));
612 FileManager.prototype.initializeUI = function(dialogDom, callback) {
613 this.dialogDom_ = dialogDom;
614 this.document_ = this.dialogDom_.ownerDocument;
616 this.initializeQueue_.add(
617 this.initEssentialUI_.bind(this),
618 ['initGeneral', 'initBackgroundPage'],
620 this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
621 ['initEssentialUI'], 'initAdditionalUI');
622 this.initializeQueue_.add(
623 this.initFileSystemUI_.bind(this),
624 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI');
626 // Run again just in case if all pending closures have completed and the
627 // queue has stopped and monitor the completion.
628 this.initializeQueue_.run(callback);
632 * Initializes general purpose basic things, which are used by other
633 * initializing methods.
635 * @param {function()} callback Completion callback.
638 FileManager.prototype.initGeneral_ = function(callback) {
639 // Initialize the application state.
640 // TODO(mtomasz): Unify window.appState with location.search format.
641 if (window.appState) {
642 this.params_ = window.appState.params || {};
643 this.initCurrentDirectoryURL_ = window.appState.currentDirectoryURL;
644 this.initSelectionURL_ = window.appState.selectionURL;
645 this.initTargetName_ = window.appState.targetName;
647 // Used by the select dialog only.
648 this.params_ = location.search ?
649 JSON.parse(decodeURIComponent(location.search.substr(1))) :
651 this.initCurrentDirectoryURL_ = this.params_.currentDirectoryURL;
652 this.initSelectionURL_ = this.params_.selectionURL;
653 this.initTargetName_ = this.params_.targetName;
656 // Initialize the member variables that depend this.params_.
657 this.dialogType = this.params_.type || DialogType.FULL_PAGE;
658 this.startupPrefName_ = 'file-manager-' + this.dialogType;
659 this.fileTypes_ = this.params_.typeList || [];
665 * Initialize the background page.
666 * @param {function()} callback Completion callback.
669 FileManager.prototype.initBackgroundPage_ = function(callback) {
670 chrome.runtime.getBackgroundPage(function(backgroundPage) {
671 this.backgroundPage_ = backgroundPage;
672 this.backgroundPage_.background.ready(function() {
673 loadTimeData.data = this.backgroundPage_.background.stringData;
680 * Initializes the VolumeManager instance.
681 * @param {function()} callback Completion callback.
684 FileManager.prototype.initVolumeManager_ = function(callback) {
685 // Auto resolving to local path does not work for folders (e.g., dialog for
686 // loading unpacked extensions).
687 var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type);
689 // If this condition is false, VolumeManagerWrapper hides all drive
690 // related event and data, even if Drive is enabled on preference.
691 // In other words, even if Drive is disabled on preference but Files.app
692 // should show Drive when it is re-enabled, then the value should be set to
694 // Note that the Drive enabling preference change is listened by
695 // DriveIntegrationService, so here we don't need to take care about it.
697 !noLocalPathResolution || !this.params_.shouldReturnLocalPath;
698 this.volumeManager_ = new VolumeManagerWrapper(
699 driveEnabled, this.backgroundPage_);
704 * One time initialization of the Files.app's essential UI elements. These
705 * elements will be shown to the user. Only visible elements should be
706 * initialized here. Any heavy operation should be avoided. Files.app's
707 * window is shown at the end of this routine.
709 * @param {function()} callback Completion callback.
712 FileManager.prototype.initEssentialUI_ = function(callback) {
713 // Record stats of dialog types. New values must NOT be inserted into the
714 // array enumerating the types. It must be in sync with
715 // FileDialogType enum in tools/metrics/histograms/histogram.xml.
716 metrics.recordEnum('Create', this.dialogType,
717 [DialogType.SELECT_FOLDER,
718 DialogType.SELECT_UPLOAD_FOLDER,
719 DialogType.SELECT_SAVEAS_FILE,
720 DialogType.SELECT_OPEN_FILE,
721 DialogType.SELECT_OPEN_MULTI_FILE,
722 DialogType.FULL_PAGE]);
724 // Create the metadata cache.
725 this.metadataCache_ = MetadataCache.createFull(this.volumeManager_);
727 // Create the root view of FileManager.
728 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType);
729 this.fileTypeSelector_ = this.ui_.fileTypeSelector;
730 this.okButton_ = this.ui_.okButton;
731 this.cancelButton_ = this.ui_.cancelButton;
733 // Show the window as soon as the UI pre-initialization is done.
734 if (this.dialogType == DialogType.FULL_PAGE &&
735 !util.platform.runningInBrowser()) {
736 chrome.app.window.current().show();
737 setTimeout(callback, 100); // Wait until the animation is finished.
744 * One-time initialization of dialogs.
747 FileManager.prototype.initDialogs_ = function() {
748 // Initialize the dialog.
749 this.ui_.initDialogs();
750 FileManagerDialogBase.setFileManager(this);
752 // Obtains the dialog instances from FileManagerUI.
753 // TODO(hirono): Remove the properties from the FileManager class.
754 this.error = this.ui_.errorDialog;
755 this.alert = this.ui_.alertDialog;
756 this.confirm = this.ui_.confirmDialog;
757 this.prompt = this.ui_.promptDialog;
758 this.shareDialog_ = this.ui_.shareDialog;
759 this.defaultTaskPicker = this.ui_.defaultTaskPicker;
760 this.suggestAppsDialog = this.ui_.suggestAppsDialog;
764 * One-time initialization of various DOM nodes. Loads the additional DOM
765 * elements visible to the user. Initialize here elements, which are expensive
766 * or hidden in the beginning.
768 * @param {function()} callback Completion callback.
771 FileManager.prototype.initAdditionalUI_ = function(callback) {
773 this.ui_.initAdditionalUI();
775 this.dialogDom_.addEventListener('drop', function(e) {
776 // Prevent opening an URL by dropping it onto the page.
780 this.dialogDom_.addEventListener('click',
781 this.onExternalLinkClick_.bind(this));
782 // Cache nodes we'll be manipulating.
783 var dom = this.dialogDom_;
785 this.filenameInput_ = dom.querySelector('#filename-input-box input');
786 this.taskItems_ = dom.querySelector('#tasks');
788 this.table_ = dom.querySelector('.detail-table');
789 this.grid_ = dom.querySelector('.thumbnail-grid');
790 this.spinner_ = dom.querySelector('#list-container > .spinner-layer');
791 this.showSpinner_(true);
793 var fullPage = this.dialogType == DialogType.FULL_PAGE;
794 FileTable.decorate(this.table_, this.metadataCache_, fullPage);
795 FileGrid.decorate(this.grid_, this.metadataCache_, this.volumeManager_);
797 this.previewPanel_ = new PreviewPanel(
798 dom.querySelector('.preview-panel'),
799 DialogType.isOpenDialog(this.dialogType) ?
800 PreviewPanel.VisibilityType.ALWAYS_VISIBLE :
801 PreviewPanel.VisibilityType.AUTO,
803 this.volumeManager_);
804 this.previewPanel_.addEventListener(
805 PreviewPanel.Event.VISIBILITY_CHANGE,
806 this.onPreviewPanelVisibilityChange_.bind(this));
807 this.previewPanel_.initialize();
809 this.previewPanel_.breadcrumbs.addEventListener(
810 'pathclick', this.onBreadcrumbClick_.bind(this));
812 // Initialize progress center panel.
813 this.progressCenterPanel_ = new ProgressCenterPanel(
814 dom.querySelector('#progress-center'));
815 this.backgroundPage_.background.progressCenter.addPanel(
816 this.progressCenterPanel_);
818 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
819 this.document_.addEventListener('keyup', this.onKeyUp_.bind(this));
821 this.renameInput_ = this.document_.createElement('input');
822 this.renameInput_.className = 'rename';
824 this.renameInput_.addEventListener(
825 'keydown', this.onRenameInputKeyDown_.bind(this));
826 this.renameInput_.addEventListener(
827 'blur', this.onRenameInputBlur_.bind(this));
829 // TODO(hirono): Rename the handler after creating the DialogFooter class.
830 this.filenameInput_.addEventListener(
831 'input', this.onFilenameInputInput_.bind(this));
832 this.filenameInput_.addEventListener(
833 'keydown', this.onFilenameInputKeyDown_.bind(this));
834 this.filenameInput_.addEventListener(
835 'focus', this.onFilenameInputFocus_.bind(this));
837 this.listContainer_ = this.dialogDom_.querySelector('#list-container');
838 this.listContainer_.addEventListener(
839 'keydown', this.onListKeyDown_.bind(this));
840 this.listContainer_.addEventListener(
841 'keypress', this.onListKeyPress_.bind(this));
842 this.listContainer_.addEventListener(
843 'mousemove', this.onListMouseMove_.bind(this));
845 this.okButton_.addEventListener('click', this.onOk_.bind(this));
846 this.onCancelBound_ = this.onCancel_.bind(this);
847 this.cancelButton_.addEventListener('click', this.onCancelBound_);
849 this.decorateSplitter(
850 this.dialogDom_.querySelector('#navigation-list-splitter'));
851 this.decorateSplitter(
852 this.dialogDom_.querySelector('#middlebar-splitter'));
854 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
856 this.syncButton = this.dialogDom_.querySelector(
857 '#gear-menu-drive-sync-settings');
858 this.hostedButton = this.dialogDom_.querySelector(
859 '#gear-menu-drive-hosted-settings');
861 this.detailViewButton_ =
862 this.dialogDom_.querySelector('#detail-view');
863 this.detailViewButton_.addEventListener('activate',
864 this.onDetailViewButtonClick_.bind(this));
866 this.thumbnailViewButton_ =
867 this.dialogDom_.querySelector('#thumbnail-view');
868 this.thumbnailViewButton_.addEventListener('activate',
869 this.onThumbnailViewButtonClick_.bind(this));
871 cr.ui.ComboButton.decorate(this.taskItems_);
872 this.taskItems_.showMenu = function(shouldSetFocus) {
873 // Prevent the empty menu from opening.
874 if (!this.menu.length)
876 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus);
878 this.taskItems_.addEventListener('select',
879 this.onTaskItemClicked_.bind(this));
881 this.dialogDom_.ownerDocument.defaultView.addEventListener(
882 'resize', this.onResize_.bind(this));
884 this.filePopup_ = null;
886 this.searchBoxWrapper_ = this.ui_.searchBox.element;
887 this.searchBox_ = this.ui_.searchBox.inputElement;
888 this.searchBox_.addEventListener(
889 'input', this.onSearchBoxUpdate_.bind(this));
890 this.ui_.searchBox.clearButton.addEventListener(
891 'click', this.onSearchClearButtonClick_.bind(this));
893 this.lastSearchQuery_ = '';
895 this.autocompleteList_ = this.ui_.searchBox.autocompleteList;
896 this.autocompleteList_.requestSuggestions =
897 this.requestAutocompleteSuggestions_.bind(this);
899 // Instead, open the suggested item when Enter key is pressed or
901 this.autocompleteList_.handleEnterKeydown = function(event) {
902 this.openAutocompleteSuggestion_();
903 this.lastAutocompleteQuery_ = '';
904 this.autocompleteList_.suggestions = [];
906 this.autocompleteList_.addEventListener('mousedown', function(event) {
907 this.openAutocompleteSuggestion_();
908 this.lastAutocompleteQuery_ = '';
909 this.autocompleteList_.suggestions = [];
912 this.defaultActionMenuItem_ =
913 this.dialogDom_.querySelector('#default-action');
915 this.openWithCommand_ =
916 this.dialogDom_.querySelector('#open-with');
918 this.driveBuyMoreStorageCommand_ =
919 this.dialogDom_.querySelector('#drive-buy-more-space');
921 this.defaultActionMenuItem_.addEventListener('activate',
922 this.dispatchSelectionAction_.bind(this));
924 this.initFileTypeFilter_();
926 util.addIsFocusedMethod();
928 // Populate the static localized strings.
929 i18nTemplate.process(this.document_, loadTimeData);
931 // Arrange the file list.
932 this.table_.normalizeColumns();
933 this.table_.redraw();
939 * @param {Event} event Click event.
942 FileManager.prototype.onBreadcrumbClick_ = function(event) {
943 this.directoryModel_.changeDirectoryEntry(event.entry);
947 * Constructs table and grid (heavy operation).
950 FileManager.prototype.initFileList_ = function() {
951 // Always sharing the data model between the detail/thumb views confuses
952 // them. Instead we maintain this bogus data model, and hook it up to the
953 // view that is not in use.
954 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
955 this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
957 var singleSelection =
958 this.dialogType == DialogType.SELECT_OPEN_FILE ||
959 this.dialogType == DialogType.SELECT_FOLDER ||
960 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
961 this.dialogType == DialogType.SELECT_SAVEAS_FILE;
963 this.fileFilter_ = new FileFilter(
965 false /* Don't show dot files by default. */);
967 this.fileWatcher_ = new FileWatcher(this.metadataCache_);
968 this.fileWatcher_.addEventListener(
969 'watcher-metadata-changed',
970 this.onWatcherMetadataChanged_.bind(this));
972 this.directoryModel_ = new DirectoryModel(
977 this.volumeManager_);
979 this.folderShortcutsModel_ = new FolderShortcutsDataModel(
980 this.volumeManager_);
982 this.selectionHandler_ = new FileSelectionHandler(this);
984 var dataModel = this.directoryModel_.getFileList();
986 this.table_.setupCompareFunctions(dataModel);
988 dataModel.addEventListener('permuted',
989 this.updateStartupPrefs_.bind(this));
991 this.directoryModel_.getFileListSelection().addEventListener('change',
992 this.selectionHandler_.onFileSelectionChanged.bind(
993 this.selectionHandler_));
995 this.initList_(this.grid_);
996 this.initList_(this.table_.list);
998 var fileListFocusBound = this.onFileListFocus_.bind(this);
999 this.table_.list.addEventListener('focus', fileListFocusBound);
1000 this.grid_.addEventListener('focus', fileListFocusBound);
1002 var dragStartBound = this.onDragStart_.bind(this);
1003 this.table_.list.addEventListener('dragstart', dragStartBound);
1004 this.grid_.addEventListener('dragstart', dragStartBound);
1006 var dragEndBound = this.onDragEnd_.bind(this);
1007 this.table_.list.addEventListener('dragend', dragEndBound);
1008 this.grid_.addEventListener('dragend', dragEndBound);
1009 // This event is published by DragSelector because drag end event is not
1010 // published at the end of drag selection.
1011 this.table_.list.addEventListener('dragselectionend', dragEndBound);
1012 this.grid_.addEventListener('dragselectionend', dragEndBound);
1014 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
1015 // attach the directory model.
1016 this.initNavigationList_();
1018 this.table_.addEventListener('column-resize-end',
1019 this.updateStartupPrefs_.bind(this));
1021 // Restore preferences.
1022 this.directoryModel_.getFileList().sort(
1023 this.viewOptions_.sortField || 'modificationTime',
1024 this.viewOptions_.sortDirection || 'desc');
1025 if (this.viewOptions_.columns) {
1026 var cm = this.table_.columnModel;
1027 for (var i = 0; i < cm.totalSize; i++) {
1028 if (this.viewOptions_.columns[i] > 0)
1029 cm.setWidth(i, this.viewOptions_.columns[i]);
1032 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
1034 this.textSearchState_ = {text: '', date: new Date()};
1035 this.closeOnUnmount_ = (this.params_.action == 'auto-open');
1037 if (this.closeOnUnmount_) {
1038 this.volumeManager_.addEventListener('externally-unmounted',
1039 this.onExternallyUnmounted_.bind(this));
1042 // Update metadata to change 'Today' and 'Yesterday' dates.
1043 var today = new Date();
1045 today.setMinutes(0);
1046 today.setSeconds(0);
1047 today.setMilliseconds(0);
1048 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1049 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
1055 FileManager.prototype.initNavigationList_ = function() {
1056 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
1057 DirectoryTree.decorate(this.directoryTree_,
1058 this.directoryModel_,
1059 this.volumeManager_);
1061 this.navigationList_ = this.dialogDom_.querySelector('#navigation-list');
1062 NavigationList.decorate(this.navigationList_,
1063 this.volumeManager_,
1064 this.directoryModel_);
1065 this.navigationList_.fileManager = this;
1066 this.navigationList_.dataModel = new NavigationListModel(
1067 this.volumeManager_, this.folderShortcutsModel_);
1073 FileManager.prototype.updateMiddleBarVisibility_ = function() {
1074 var entry = this.directoryModel_.getCurrentDirEntry();
1078 var driveVolume = this.volumeManager_.getVolumeInfo(entry);
1079 var visible = driveVolume && !driveVolume.error &&
1080 driveVolume.volumeType === VolumeManagerCommon.VolumeType.DRIVE;
1082 querySelector('.dialog-middlebar-contents').hidden = !visible;
1083 this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible;
1090 FileManager.prototype.updateStartupPrefs_ = function() {
1091 var sortStatus = this.directoryModel_.getFileList().sortStatus;
1093 sortField: sortStatus.field,
1094 sortDirection: sortStatus.direction,
1096 listType: this.listType_
1098 var cm = this.table_.columnModel;
1099 for (var i = 0; i < cm.totalSize; i++) {
1100 prefs.columns.push(cm.getWidth(i));
1102 // Save the global default.
1103 util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs));
1105 // Save the window-specific preference.
1106 if (window.appState) {
1107 window.appState.viewOptions = prefs;
1108 util.saveAppState();
1112 FileManager.prototype.refocus = function() {
1114 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
1115 targetElement = this.filenameInput_;
1117 targetElement = this.currentList_;
1119 // Hack: if the tabIndex is disabled, we can assume a modal dialog is
1120 // shown. Focus to a button on the dialog instead.
1121 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1)
1122 targetElement = document.querySelector('button:not([tabIndex="-1"])');
1125 targetElement.focus();
1129 * File list focus handler. Used to select the top most element on the list
1130 * if nothing was selected.
1134 FileManager.prototype.onFileListFocus_ = function() {
1135 // If the file list is focused by <Tab>, select the first item if no item
1137 if (this.pressingTab_) {
1138 if (this.getSelection() && this.getSelection().totalCount == 0)
1139 this.directoryModel_.selectIndex(0);
1144 * Index of selected item in the typeList of the dialog params.
1146 * @return {number} 1-based index of selected type or 0 if no type selected.
1149 FileManager.prototype.getSelectedFilterIndex_ = function() {
1150 var index = Number(this.fileTypeSelector_.selectedIndex);
1151 if (index < 0) // Nothing selected.
1153 if (this.params_.includeAllFiles) // Already 1-based.
1155 return index + 1; // Convert to 1-based;
1158 FileManager.prototype.setListType = function(type) {
1159 if (type && type == this.listType_)
1162 this.table_.list.startBatchUpdates();
1163 this.grid_.startBatchUpdates();
1165 // TODO(dzvorygin): style.display and dataModel setting order shouldn't
1166 // cause any UI bugs. Currently, the only right way is first to set display
1167 // style and only then set dataModel.
1169 if (type == FileManager.ListType.DETAIL) {
1170 this.table_.dataModel = this.directoryModel_.getFileList();
1171 this.table_.selectionModel = this.directoryModel_.getFileListSelection();
1172 this.table_.hidden = false;
1173 this.grid_.hidden = true;
1174 this.grid_.selectionModel = this.emptySelectionModel_;
1175 this.grid_.dataModel = this.emptyDataModel_;
1176 this.table_.hidden = false;
1177 /** @type {cr.ui.List} */
1178 this.currentList_ = this.table_.list;
1179 this.detailViewButton_.setAttribute('checked', '');
1180 this.thumbnailViewButton_.removeAttribute('checked');
1181 this.detailViewButton_.setAttribute('disabled', '');
1182 this.thumbnailViewButton_.removeAttribute('disabled');
1183 } else if (type == FileManager.ListType.THUMBNAIL) {
1184 this.grid_.dataModel = this.directoryModel_.getFileList();
1185 this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
1186 this.grid_.hidden = false;
1187 this.table_.hidden = true;
1188 this.table_.selectionModel = this.emptySelectionModel_;
1189 this.table_.dataModel = this.emptyDataModel_;
1190 this.grid_.hidden = false;
1191 /** @type {cr.ui.List} */
1192 this.currentList_ = this.grid_;
1193 this.thumbnailViewButton_.setAttribute('checked', '');
1194 this.detailViewButton_.removeAttribute('checked');
1195 this.thumbnailViewButton_.setAttribute('disabled', '');
1196 this.detailViewButton_.removeAttribute('disabled');
1198 throw new Error('Unknown list type: ' + type);
1201 this.listType_ = type;
1202 this.updateStartupPrefs_();
1205 this.table_.list.endBatchUpdates();
1206 this.grid_.endBatchUpdates();
1210 * Initialize the file list table or grid.
1212 * @param {cr.ui.List} list The list.
1215 FileManager.prototype.initList_ = function(list) {
1216 // Overriding the default role 'list' to 'listbox' for better accessibility
1218 list.setAttribute('role', 'listbox');
1219 list.addEventListener('click', this.onDetailClick_.bind(this));
1220 list.id = 'file-list';
1226 FileManager.prototype.onCopyProgress_ = function(event) {
1227 if (event.reason == 'ERROR' &&
1228 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
1229 event.error.data.toDrive &&
1230 event.error.data.name == util.FileError.QUOTA_EXCEEDED_ERR) {
1231 this.alert.showHtml(
1232 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
1233 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
1235 event.error.data.sourceFileUrl.split('/').pop()),
1236 str('GOOGLE_DRIVE_BUY_STORAGE_URL')));
1241 * Handler of file manager operations. Called when an entry has been
1243 * This updates directory model to reflect operation result immediately (not
1244 * waiting for directory update event). Also, preloads thumbnails for the
1245 * images of new entries.
1246 * See also FileOperationManager.EventRouter.
1248 * @param {Event} event An event for the entry change.
1251 FileManager.prototype.onEntryChanged_ = function(event) {
1252 var kind = event.kind;
1253 var entry = event.entry;
1254 this.directoryModel_.onEntryChanged(kind, entry);
1255 this.selectionHandler_.onFileSelectionChanged();
1257 if (kind === util.EntryChangedKind.CREATED && FileType.isImage(entry)) {
1258 // Preload a thumbnail if the new copied entry an image.
1259 var locationInfo = this.volumeManager_.getLocationInfo(entry);
1262 this.metadataCache_.get(entry, 'thumbnail|drive', function(metadata) {
1263 var thumbnailLoader_ = new ThumbnailLoader(
1265 ThumbnailLoader.LoaderType.CANVAS,
1267 undefined, // Media type.
1268 // TODO(mtomasz): Use Entry instead of paths.
1269 locationInfo.isDriveBased ?
1270 ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
1271 ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
1272 10); // Very low priority.
1273 thumbnailLoader_.loadDetachedImage(function(success) {});
1279 * Fills the file type list or hides it.
1282 FileManager.prototype.initFileTypeFilter_ = function() {
1283 if (this.params_.includeAllFiles) {
1284 var option = this.document_.createElement('option');
1285 option.innerText = str('ALL_FILES_FILTER');
1286 this.fileTypeSelector_.appendChild(option);
1290 for (var i = 0; i !== this.fileTypes_.length; i++) {
1291 var fileType = this.fileTypes_[i];
1292 var option = this.document_.createElement('option');
1293 var description = fileType.description;
1295 // See if all the extensions in the group have the same description.
1296 for (var j = 0; j !== fileType.extensions.length; j++) {
1297 var currentDescription = FileType.typeToString(
1298 FileType.getTypeForName('.' + fileType.extensions[j]));
1299 if (!description) // Set the first time.
1300 description = currentDescription;
1301 else if (description != currentDescription) {
1302 // No single description, fall through to the extension list.
1309 // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1310 description = fileType.extensions.map(function(s) {
1314 option.innerText = description;
1316 option.value = i + 1;
1318 if (fileType.selected)
1319 option.selected = true;
1321 this.fileTypeSelector_.appendChild(option);
1324 var options = this.fileTypeSelector_.querySelectorAll('option');
1325 if (options.length >= 2) {
1326 // There is in fact no choice, show the selector.
1327 this.fileTypeSelector_.hidden = false;
1329 this.fileTypeSelector_.addEventListener('change',
1330 this.updateFileTypeFilter_.bind(this));
1335 * Filters file according to the selected file type.
1338 FileManager.prototype.updateFileTypeFilter_ = function() {
1339 this.fileFilter_.removeFilter('fileType');
1340 var selectedIndex = this.getSelectedFilterIndex_();
1341 if (selectedIndex > 0) { // Specific filter selected.
1342 var regexp = new RegExp('.*(' +
1343 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
1344 var filter = function(entry) {
1345 return entry.isDirectory || regexp.test(entry.name);
1347 this.fileFilter_.addFilter('fileType', filter);
1352 * Resize details and thumb views to fit the new window size.
1355 FileManager.prototype.onResize_ = function() {
1356 if (this.listType_ == FileManager.ListType.THUMBNAIL)
1357 this.grid_.relayout();
1359 this.table_.relayout();
1361 // May not be available during initialization.
1362 if (this.directoryTree_)
1363 this.directoryTree_.relayout();
1365 // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before
1366 // file system is available.
1367 if (this.navigationList_)
1368 this.navigationList_.redraw();
1370 this.previewPanel_.breadcrumbs.truncate();
1374 * Handles local metadata changes in the currect directory.
1375 * @param {Event} event Change event.
1378 FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
1379 this.updateMetadataInUI_(
1380 event.metadataType, event.entries, event.properties);
1384 * Resize details and thumb views to fit the new window size.
1387 FileManager.prototype.onPreviewPanelVisibilityChange_ = function() {
1388 // This method may be called on initialization. Some object may not be
1391 var panelHeight = this.previewPanel_.visible ?
1392 this.previewPanel_.height : 0;
1394 this.grid_.setBottomMarginForPanel(panelHeight);
1396 this.table_.setBottomMarginForPanel(panelHeight);
1398 if (this.directoryTree_) {
1399 this.directoryTree_.setBottomMarginForPanel(panelHeight);
1400 this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
1405 * Invoked when the drag is started on the list or the grid.
1408 FileManager.prototype.onDragStart_ = function() {
1409 // On open file dialog, the preview panel is always shown.
1410 if (DialogType.isOpenDialog(this.dialogType))
1412 this.previewPanel_.visibilityType =
1413 PreviewPanel.VisibilityType.ALWAYS_HIDDEN;
1417 * Invoked when the drag is ended on the list or the grid.
1420 FileManager.prototype.onDragEnd_ = function() {
1421 // On open file dialog, the preview panel is always shown.
1422 if (DialogType.isOpenDialog(this.dialogType))
1424 this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO;
1428 * Sets up the current directory during initialization.
1431 FileManager.prototype.setupCurrentDirectory_ = function() {
1432 var tracker = this.directoryModel_.createDirectoryChangeTracker();
1433 var queue = new AsyncUtil.Queue();
1435 // Wait until the volume manager is initialized.
1436 queue.run(function(callback) {
1438 this.volumeManager_.ensureInitialized(callback);
1441 var nextCurrentDirEntry;
1444 // Resolve the selectionURL to selectionEntry or to currentDirectoryEntry
1445 // in case of being a display root.
1446 queue.run(function(callback) {
1447 if (!this.initSelectionURL_) {
1451 webkitResolveLocalFileSystemURL(
1452 this.initSelectionURL_,
1454 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1455 // If location information is not available, then the volume is
1456 // no longer (or never) available.
1457 if (!locationInfo) {
1461 // If the selection is root, then use it as a current directory
1462 // instead. This is because, selecting a root entry is done as
1464 if (locationInfo.isRootEntry)
1465 nextCurrentDirEntry = inEntry;
1467 selectionEntry = inEntry;
1469 }.bind(this), callback);
1471 // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done
1472 // by the previous step).
1473 queue.run(function(callback) {
1474 if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) {
1478 webkitResolveLocalFileSystemURL(
1479 this.initCurrentDirectoryURL_,
1481 var locationInfo = this.volumeManager_.getLocationInfo(inEntry);
1482 if (!locationInfo) {
1486 nextCurrentDirEntry = inEntry;
1488 }.bind(this), callback);
1489 // TODO(mtomasz): Implement reopening on special search, when fake
1490 // entries are converted to directory providers.
1493 // If the directory to be changed to is not available, then first fallback
1494 // to the parent of the selection entry.
1495 queue.run(function(callback) {
1496 if (nextCurrentDirEntry || !selectionEntry) {
1500 selectionEntry.getParent(function(inEntry) {
1501 nextCurrentDirEntry = inEntry;
1506 // If the directory to be changed to is still not resolved, then fallback
1507 // to the default display root.
1508 queue.run(function(callback) {
1509 if (nextCurrentDirEntry) {
1513 this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) {
1514 nextCurrentDirEntry = displayRoot;
1519 // If selection failed to be resolved (eg. didn't exist, in case of saving
1520 // a file, or in case of a fallback of the current directory, then try to
1521 // resolve again using the target name.
1522 queue.run(function(callback) {
1523 if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) {
1527 // Try to resolve as a file first. If it fails, then as a directory.
1528 nextCurrentDirEntry.getFile(
1529 this.initTargetName_,
1531 function(targetEntry) {
1532 selectionEntry = targetEntry;
1535 // Failed to resolve as a file
1536 nextCurrentDirEntry.getDirectory(
1537 this.initTargetName_,
1539 function(targetEntry) {
1540 selectionEntry = targetEntry;
1543 // Failed to resolve as either file or directory.
1551 queue.run(function(callback) {
1552 // Check directory change.
1554 if (tracker.hasChanged) {
1558 // Finish setup current directory.
1559 this.finishSetupCurrentDirectory_(
1560 nextCurrentDirEntry,
1562 this.initTargetName_);
1568 * @param {DirectoryEntry} directoryEntry Directory to be opened.
1569 * @param {Entry=} opt_selectionEntry Entry to be selected.
1570 * @param {string=} opt_suggestedName Suggested name for a non-existing\
1574 FileManager.prototype.finishSetupCurrentDirectory_ = function(
1575 directoryEntry, opt_selectionEntry, opt_suggestedName) {
1576 // Open the directory, and select the selection (if passed).
1577 if (util.isFakeEntry(directoryEntry)) {
1578 this.directoryModel_.specialSearch(directoryEntry, '');
1580 this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
1581 if (opt_selectionEntry)
1582 this.directoryModel_.selectEntry(opt_selectionEntry);
1586 if (this.dialogType === DialogType.FULL_PAGE) {
1587 // In the FULL_PAGE mode if the restored URL points to a file we might
1588 // have to invoke a task after selecting it.
1589 if (this.params_.action === 'select')
1593 // Handle restoring after crash, or the gallery action.
1594 // TODO(mtomasz): Use the gallery action instead of just the gallery
1596 if (this.params_.gallery ||
1597 this.params_.action === 'gallery' ||
1598 this.params_.action === 'gallery-video') {
1599 if (!opt_selectionEntry) {
1600 // Non-existent file or a directory.
1601 // Reloading while the Gallery is open with empty or multiple
1602 // selection. Open the Gallery when the directory is scanned.
1604 new FileTasks(this, this.params_).openGallery([]);
1607 // The file or the directory exists.
1609 new FileTasks(this, this.params_).openGallery([opt_selectionEntry]);
1613 // TODO(mtomasz): Implement remounting archives after crash.
1614 // See: crbug.com/333139
1617 // If there is a task to be run, run it after the scan is completed.
1619 var listener = function() {
1620 if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(),
1622 // Opened on a different URL. Probably fallbacked. Therefore,
1623 // do not invoke a task.
1626 this.directoryModel_.removeEventListener(
1627 'scan-completed', listener);
1630 this.directoryModel_.addEventListener('scan-completed', listener);
1632 } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) {
1633 this.filenameInput_.value = opt_suggestedName || '';
1634 this.selectTargetNameInFilenameInput_();
1641 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
1642 var entries = this.directoryModel_.getFileList().slice();
1643 var directoryEntry = this.directoryModel_.getCurrentDirEntry();
1644 if (!directoryEntry)
1646 // We don't pass callback here. When new metadata arrives, we have an
1647 // observer registered to update the UI.
1649 // TODO(dgozman): refresh content metadata only when modificationTime
1651 var isFakeEntry = util.isFakeEntry(directoryEntry);
1652 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
1654 this.metadataCache_.clearRecursively(directoryEntry, '*');
1655 this.metadataCache_.get(getEntries, 'filesystem', null);
1657 if (this.isOnDrive())
1658 this.metadataCache_.get(getEntries, 'drive', null);
1660 var visibleItems = this.currentList_.items;
1661 var visibleEntries = [];
1662 for (var i = 0; i < visibleItems.length; i++) {
1663 var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
1664 var entry = this.directoryModel_.getFileList().item(index);
1665 // The following check is a workaround for the bug in list: sometimes item
1666 // does not have listIndex, and therefore is not found in the list.
1667 if (entry) visibleEntries.push(entry);
1669 this.metadataCache_.get(visibleEntries, 'thumbnail', null);
1675 FileManager.prototype.dailyUpdateModificationTime_ = function() {
1676 var entries = this.directoryModel_.getFileList().slice();
1677 this.metadataCache_.get(
1680 this.updateMetadataInUI_.bind(this, 'filesystem', entries));
1682 setTimeout(this.dailyUpdateModificationTime_.bind(this),
1683 MILLISECONDS_IN_DAY);
1687 * @param {string} type Type of metadata changed.
1688 * @param {Array.<Entry>} entries Array of entries.
1689 * @param {Object.<string, Object>} props Map from entry URLs to metadata
1693 FileManager.prototype.updateMetadataInUI_ = function(
1694 type, entries, properties) {
1695 if (this.listType_ == FileManager.ListType.DETAIL)
1696 this.table_.updateListItemsMetadata(type, properties);
1698 this.grid_.updateListItemsMetadata(type, properties);
1699 // TODO: update bottom panel thumbnails.
1703 * Restore the item which is being renamed while refreshing the file list. Do
1704 * nothing if no item is being renamed or such an item disappeared.
1706 * While refreshing file list it gets repopulated with new file entries.
1707 * There is not a big difference whether DOM items stay the same or not.
1708 * Except for the item that the user is renaming.
1712 FileManager.prototype.restoreItemBeingRenamed_ = function() {
1713 if (!this.isRenamingInProgress())
1716 var dm = this.directoryModel_;
1717 var leadIndex = dm.getFileListSelection().leadIndex;
1721 var leadEntry = dm.getFileList().item(leadIndex);
1722 if (!util.isSameEntry(this.renameInput_.currentEntry, leadEntry))
1725 var leadListItem = this.findListItemForNode_(this.renameInput_);
1726 if (this.currentList_ == this.table_.list) {
1727 this.table_.updateFileMetadata(leadListItem, leadEntry);
1729 this.currentList_.restoreLeadItem(leadListItem);
1733 * TODO(mtomasz): Move this to a utility function working on the root type.
1734 * @return {boolean} True if the current directory content is from Google
1737 FileManager.prototype.isOnDrive = function() {
1738 var rootType = this.directoryModel_.getCurrentRootType();
1739 return rootType === VolumeManagerCommon.RootType.DRIVE ||
1740 rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME ||
1741 rootType === VolumeManagerCommon.RootType.DRIVE_RECENT ||
1742 rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE;
1746 * Check if the drive-related setting items should be shown on currently
1747 * displayed gear menu.
1748 * @return {boolean} True if those setting items should be shown.
1750 FileManager.prototype.shouldShowDriveSettings = function() {
1751 return this.isOnDrive() && this.isSecretGearMenuShown_;
1755 * Overrides default handling for clicks on hyperlinks.
1756 * In a packaged apps links with targer='_blank' open in a new tab by
1757 * default, other links do not open at all.
1759 * @param {Event} event Click event.
1762 FileManager.prototype.onExternalLinkClick_ = function(event) {
1763 if (event.target.tagName != 'A' || !event.target.href)
1766 if (this.dialogType != DialogType.FULL_PAGE)
1771 * Task combobox handler.
1773 * @param {Object} event Event containing task which was clicked.
1776 FileManager.prototype.onTaskItemClicked_ = function(event) {
1777 var selection = this.getSelection();
1778 if (!selection.tasks) return;
1780 if (event.item.task) {
1781 // Task field doesn't exist on change-default dropdown item.
1782 selection.tasks.execute(event.item.task.taskId);
1784 var extensions = [];
1786 for (var i = 0; i < selection.entries.length; i++) {
1787 var match = /\.(\w+)$/g.exec(selection.entries[i].toURL());
1789 var ext = match[1].toUpperCase();
1790 if (extensions.indexOf(ext) == -1) {
1791 extensions.push(ext);
1798 if (extensions.length == 1) {
1799 format = extensions[0];
1802 // Change default was clicked. We should open "change default" dialog.
1803 selection.tasks.showTaskPicker(this.defaultTaskPicker,
1804 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
1805 strf('CHANGE_DEFAULT_CAPTION', format),
1806 this.onDefaultTaskDone_.bind(this));
1811 * Sets the given task as default, when this task is applicable.
1813 * @param {Object} task Task to set as default.
1816 FileManager.prototype.onDefaultTaskDone_ = function(task) {
1817 // TODO(dgozman): move this method closer to tasks.
1818 var selection = this.getSelection();
1819 chrome.fileBrowserPrivate.setDefaultTask(
1821 util.entriesToURLs(selection.entries),
1822 selection.mimeTypes);
1823 selection.tasks = new FileTasks(this);
1824 selection.tasks.init(selection.entries, selection.mimeTypes);
1825 selection.tasks.display(this.taskItems_);
1826 this.refreshCurrentDirectoryMetadata_();
1827 this.selectionHandler_.onFileSelectionChanged();
1833 FileManager.prototype.onPreferencesChanged_ = function() {
1835 this.getPreferences_(function(prefs) {
1836 self.initDateTimeFormatters_();
1837 self.refreshCurrentDirectoryMetadata_();
1839 if (prefs.cellularDisabled)
1840 self.syncButton.setAttribute('checked', '');
1842 self.syncButton.removeAttribute('checked');
1844 if (self.hostedButton.hasAttribute('checked') ===
1845 prefs.hostedFilesDisabled && self.isOnDrive()) {
1846 self.directoryModel_.rescan();
1849 if (!prefs.hostedFilesDisabled)
1850 self.hostedButton.setAttribute('checked', '');
1852 self.hostedButton.removeAttribute('checked');
1854 true /* refresh */);
1857 FileManager.prototype.onDriveConnectionChanged_ = function() {
1858 var connection = this.volumeManager_.getDriveConnectionState();
1859 if (this.commandHandler)
1860 this.commandHandler.updateAvailability();
1861 if (this.dialogContainer_)
1862 this.dialogContainer_.setAttribute('connection', connection.type);
1863 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
1864 this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
1868 * Tells whether the current directory is read only.
1869 * TODO(mtomasz): Remove and use EntryLocation directly.
1870 * @return {boolean} True if read only, false otherwise.
1872 FileManager.prototype.isOnReadonlyDirectory = function() {
1873 return this.directoryModel_.isReadOnly();
1877 * @param {Event} Unmount event.
1880 FileManager.prototype.onExternallyUnmounted_ = function(event) {
1881 if (event.volumeInfo === this.currentVolumeInfo_) {
1882 if (this.closeOnUnmount_) {
1883 // If the file manager opened automatically when a usb drive inserted,
1884 // user have never changed current volume (that implies the current
1885 // directory is still on the device) then close this window.
1892 * Shows a modal-like file viewer/editor on top of the File Manager UI.
1894 * @param {HTMLElement} popup Popup element.
1895 * @param {function()} closeCallback Function to call after the popup is
1898 FileManager.prototype.openFilePopup = function(popup, closeCallback) {
1899 this.closeFilePopup();
1900 this.filePopup_ = popup;
1901 this.filePopupCloseCallback_ = closeCallback;
1902 this.dialogDom_.insertBefore(
1903 this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area'));
1904 this.filePopup_.focus();
1905 this.document_.body.setAttribute('overlay-visible', '');
1906 this.document_.querySelector('#iframe-drag-area').hidden = false;
1910 * Closes the modal-like file viewer/editor popup.
1912 FileManager.prototype.closeFilePopup = function() {
1913 if (this.filePopup_) {
1914 this.document_.body.removeAttribute('overlay-visible');
1915 this.document_.querySelector('#iframe-drag-area').hidden = true;
1916 // The window resize would not be processed properly while the relevant
1917 // divs had 'display:none', force resize after the layout fired.
1918 setTimeout(this.onResize_.bind(this), 0);
1919 if (this.filePopup_.contentWindow &&
1920 this.filePopup_.contentWindow.unload) {
1921 this.filePopup_.contentWindow.unload();
1924 if (this.filePopupCloseCallback_) {
1925 this.filePopupCloseCallback_();
1926 this.filePopupCloseCallback_ = null;
1929 // These operations have to be in the end, otherwise v8 crashes on an
1930 // assert. See: crbug.com/224174.
1931 this.dialogDom_.removeChild(this.filePopup_);
1932 this.filePopup_ = null;
1937 * Updates visibility of the draggable app region in the modal-like file
1940 * @param {boolean} visible True for visible, false otherwise.
1942 FileManager.prototype.onFilePopupAppRegionChanged = function(visible) {
1943 if (!this.filePopup_)
1946 this.document_.querySelector('#iframe-drag-area').hidden = !visible;
1950 * @return {Array.<Entry>} List of all entries in the current directory.
1952 FileManager.prototype.getAllEntriesInCurrentDirectory = function() {
1953 return this.directoryModel_.getFileList().slice();
1956 FileManager.prototype.isRenamingInProgress = function() {
1957 return !!this.renameInput_.currentEntry;
1963 FileManager.prototype.focusCurrentList_ = function() {
1964 if (this.listType_ == FileManager.ListType.DETAIL)
1965 this.table_.focus();
1966 else // this.listType_ == FileManager.ListType.THUMBNAIL)
1971 * Return DirectoryEntry of the current directory or null.
1972 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
1973 * null if the directory model is not ready or the current directory is
1976 FileManager.prototype.getCurrentDirectoryEntry = function() {
1977 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
1981 * Deletes the selected file and directories recursively.
1983 FileManager.prototype.deleteSelection = function() {
1984 // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364
1985 var entries = this.getSelection().entries;
1986 var message = entries.length == 1 ?
1987 strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) :
1988 strf('GALLERY_CONFIRM_DELETE_SOME', entries.length);
1989 this.confirm.show(message, function() {
1990 this.fileOperationManager_.deleteEntries(entries);
1995 * Shows the share dialog for the selected file or directory.
1997 FileManager.prototype.shareSelection = function() {
1998 var entries = this.getSelection().entries;
1999 if (entries.length != 1) {
2000 console.warn('Unable to share multiple items at once.');
2003 // Add the overlapped class to prevent the applicaiton window from
2004 // captureing mouse events.
2005 this.shareDialog_.show(entries[0], function(result) {
2006 if (result == ShareDialog.Result.NETWORK_ERROR)
2007 this.error.show(str('SHARE_ERROR'));
2012 * Creates a folder shortcut.
2013 * @param {Entry} entry A shortcut which refers to |entry| to be created.
2015 FileManager.prototype.createFolderShortcut = function(entry) {
2017 if (this.folderShortcutExists(entry))
2020 this.folderShortcutsModel_.add(entry);
2024 * Checkes if the shortcut which refers to the given folder exists or not.
2025 * @param {Entry} entry Entry of the folder to be checked.
2027 FileManager.prototype.folderShortcutExists = function(entry) {
2028 return this.folderShortcutsModel_.exists(entry);
2032 * Removes the folder shortcut.
2033 * @param {Entry} entry The shortcut which refers to |entry| is to be removed.
2035 FileManager.prototype.removeFolderShortcut = function(entry) {
2036 this.folderShortcutsModel_.remove(entry);
2040 * Blinks the selection. Used to give feedback when copying or cutting the
2043 FileManager.prototype.blinkSelection = function() {
2044 var selection = this.getSelection();
2045 if (!selection || selection.totalCount == 0)
2048 for (var i = 0; i < selection.entries.length; i++) {
2049 var selectedIndex = selection.indexes[i];
2050 var listItem = this.currentList_.getListItemByIndex(selectedIndex);
2052 this.blinkListItem_(listItem);
2057 * @param {Element} listItem List item element.
2060 FileManager.prototype.blinkListItem_ = function(listItem) {
2061 listItem.classList.add('blink');
2062 setTimeout(function() {
2063 listItem.classList.remove('blink');
2070 FileManager.prototype.selectTargetNameInFilenameInput_ = function() {
2071 var input = this.filenameInput_;
2073 var selectionEnd = input.value.lastIndexOf('.');
2074 if (selectionEnd == -1) {
2077 input.selectionStart = 0;
2078 input.selectionEnd = selectionEnd;
2083 * Handles mouse click or tap.
2085 * @param {Event} event The click event.
2088 FileManager.prototype.onDetailClick_ = function(event) {
2089 if (this.isRenamingInProgress()) {
2090 // Don't pay attention to clicks during a rename.
2094 var listItem = this.findListItemForEvent_(event);
2095 var selection = this.getSelection();
2096 if (!listItem || !listItem.selected || selection.totalCount != 1) {
2100 // React on double click, but only if both clicks hit the same item.
2101 // TODO(mtomasz): Simplify it, and use a double click handler if possible.
2102 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
2103 this.lastClickedItem_ = listItem;
2105 if (event.detail != clickNumber)
2108 var entry = selection.entries[0];
2109 if (entry.isDirectory) {
2110 this.onDirectoryAction_(entry);
2112 this.dispatchSelectionAction_();
2119 FileManager.prototype.dispatchSelectionAction_ = function() {
2120 if (this.dialogType == DialogType.FULL_PAGE) {
2121 var selection = this.getSelection();
2122 var tasks = selection.tasks;
2123 var urls = selection.urls;
2124 var mimeTypes = selection.mimeTypes;
2126 tasks.executeDefault();
2129 if (!this.okButton_.disabled) {
2137 * Opens the suggest file dialog.
2139 * @param {Entry} entry Entry of the file.
2140 * @param {function()} onSuccess Success callback.
2141 * @param {function()} onCancelled User-cancelled callback.
2142 * @param {function()} onFailure Failure callback.
2145 FileManager.prototype.openSuggestAppsDialog =
2146 function(entry, onSuccess, onCancelled, onFailure) {
2152 this.metadataCache_.get([entry], 'drive', function(props) {
2153 if (!props || !props[0] || !props[0].contentMimeType) {
2158 var basename = entry.name;
2159 var splitted = util.splitExtension(basename);
2160 var filename = splitted[0];
2161 var extension = splitted[1];
2162 var mime = props[0].contentMimeType;
2164 // Returns with failure if the file has neither extension nor mime.
2165 if (!extension || !mime) {
2170 var onDialogClosed = function(result) {
2172 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
2175 case SuggestAppsDialog.Result.FAILED:
2183 if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
2184 this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
2186 this.suggestAppsDialog.showByExtensionAndMime(
2187 extension, mime, onDialogClosed);
2193 * Called when a dialog is shown or hidden.
2194 * @param {boolean} flag True if a dialog is shown, false if hidden.
2196 FileManager.prototype.onDialogShownOrHidden = function(show) {
2198 // If a dialog is shown, activate the window.
2199 var appWindow = chrome.app.window.current();
2204 // Set/unset a flag to disable dragging on the title area.
2205 this.dialogContainer_.classList.toggle('disable-header-drag', show);
2209 * Executes directory action (i.e. changes directory).
2211 * @param {DirectoryEntry} entry Directory entry to which directory should be
2215 FileManager.prototype.onDirectoryAction_ = function(entry) {
2216 return this.directoryModel_.changeDirectoryEntry(entry);
2220 * Update the window title.
2223 FileManager.prototype.updateTitle_ = function() {
2224 if (this.dialogType != DialogType.FULL_PAGE)
2227 if (!this.currentVolumeInfo_)
2230 this.document_.title = this.currentVolumeInfo_.label;
2234 * Update the gear menu.
2237 FileManager.prototype.updateGearMenu_ = function() {
2238 this.refreshRemainingSpace_(true); // Show loading caption.
2242 * Update menus that move the window to the other profile's desktop.
2243 * TODO(hirono): Add the GearMenu class and make it a member of the class.
2244 * TODO(hirono): Handle the case where a profile is added while the menu is
2248 FileManager.prototype.updateVisitDesktopMenus_ = function() {
2249 var gearMenu = this.document_.querySelector('#gear-menu');
2251 this.document_.querySelector('#multi-profile-separator');
2253 // Remove existing menu items.
2255 this.document_.querySelectorAll('#gear-menu .visit-desktop');
2256 for (var i = 0; i < oldItems.length; i++) {
2257 gearMenu.removeChild(oldItems[i]);
2259 separator.hidden = true;
2261 if (this.dialogType !== DialogType.FULL_PAGE)
2264 // Obtain the profile information.
2265 chrome.fileBrowserPrivate.getProfiles(function(profiles,
2268 // Check if the menus are needed or not.
2269 var insertingPosition = separator.nextSibling;
2270 if (profiles.length === 1 && profiles[0].profileId === displayedId)
2273 separator.hidden = false;
2274 for (var i = 0; i < profiles.length; i++) {
2275 var profile = profiles[i];
2276 if (profile.profileId === displayedId)
2278 var item = this.document_.createElement('menuitem');
2279 cr.ui.MenuItem.decorate(item);
2280 gearMenu.insertBefore(item, insertingPosition);
2281 item.className = 'visit-desktop';
2282 item.label = strf('VISIT_DESKTOP_OF_USER',
2283 profile.displayName,
2285 item.addEventListener('activate', function(inProfile, event) {
2286 // Stop propagate and hide the menu manually, in order to prevent the
2287 // focus from being back to the button. (cf. http://crbug.com/248479)
2288 event.stopPropagation();
2289 this.gearButton_.hideMenu();
2290 this.gearButton_.blur();
2291 chrome.fileBrowserPrivate.visitDesktop(inProfile.profileId);
2292 }.bind(this, profile));
2298 * Refreshes space info of the current volume.
2299 * @param {boolean} showLoadingCaption Whether show loading caption or not.
2302 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
2303 if (!this.currentVolumeInfo_)
2306 var volumeSpaceInfoLabel =
2307 this.dialogDom_.querySelector('#volume-space-info-label');
2308 var volumeSpaceInnerBar =
2309 this.dialogDom_.querySelector('#volume-space-info-bar');
2310 var volumeSpaceOuterBar =
2311 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
2313 volumeSpaceInnerBar.setAttribute('pending', '');
2315 if (showLoadingCaption) {
2316 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
2317 volumeSpaceInnerBar.style.width = '100%';
2320 var currentVolumeInfo = this.currentVolumeInfo_;
2321 chrome.fileBrowserPrivate.getSizeStats(
2322 currentVolumeInfo.volumeId, function(result) {
2323 var volumeInfo = this.volumeManager_.getVolumeInfo(
2324 this.directoryModel_.getCurrentDirEntry());
2325 if (currentVolumeInfo !== this.currentVolumeInfo_)
2327 updateSpaceInfo(result,
2328 volumeSpaceInnerBar,
2329 volumeSpaceInfoLabel,
2330 volumeSpaceOuterBar);
2335 * Update the UI when the current directory changes.
2337 * @param {Event} event The directory-changed event.
2340 FileManager.prototype.onDirectoryChanged_ = function(event) {
2341 var newCurrentVolumeInfo = this.volumeManager_.getVolumeInfo(
2344 // If volume has changed, then update the gear menu.
2345 if (this.currentVolumeInfo_ !== newCurrentVolumeInfo) {
2346 this.updateGearMenu_();
2347 // If the volume has changed, and it was previously set, then do not
2348 // close on unmount anymore.
2349 if (this.currentVolumeInfo_)
2350 this.closeOnUnmount_ = false;
2353 // Remember the current volume info.
2354 this.currentVolumeInfo_ = newCurrentVolumeInfo;
2356 this.selectionHandler_.onFileSelectionChanged();
2357 this.ui_.searchBox.clear();
2358 // TODO(mtomasz): Consider remembering the selection.
2359 util.updateAppState(
2360 this.getCurrentDirectoryEntry() ?
2361 this.getCurrentDirectoryEntry().toURL() : '',
2362 '' /* selectionURL */,
2363 '' /* opt_param */);
2365 if (this.commandHandler)
2366 this.commandHandler.updateAvailability();
2368 this.updateUnformattedVolumeStatus_();
2369 this.updateTitle_();
2371 var currentEntry = this.getCurrentDirectoryEntry();
2372 this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ?
2373 null : currentEntry;
2376 FileManager.prototype.updateUnformattedVolumeStatus_ = function() {
2377 var volumeInfo = this.volumeManager_.getVolumeInfo(
2378 this.directoryModel_.getCurrentDirEntry());
2380 if (volumeInfo && volumeInfo.error) {
2381 this.dialogDom_.setAttribute('unformatted', '');
2383 var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
2384 if (volumeInfo.error ===
2385 VolumeManagerCommon.VolumeError.UNSUPPORTED_FILESYSTEM) {
2386 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
2388 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
2391 // Update 'canExecute' for format command so the format button's disabled
2392 // property is properly set.
2393 if (this.commandHandler)
2394 this.commandHandler.updateAvailability();
2396 this.dialogDom_.removeAttribute('unformatted');
2400 FileManager.prototype.findListItemForEvent_ = function(event) {
2401 return this.findListItemForNode_(event.touchedElement || event.srcElement);
2404 FileManager.prototype.findListItemForNode_ = function(node) {
2405 var item = this.currentList_.getListItemAncestor(node);
2406 // TODO(serya): list should check that.
2407 return item && this.currentList_.isItem(item) ? item : null;
2411 * Unload handler for the page.
2414 FileManager.prototype.onUnload_ = function() {
2415 if (this.directoryModel_)
2416 this.directoryModel_.dispose();
2417 if (this.volumeManager_)
2418 this.volumeManager_.dispose();
2419 if (this.filePopup_ &&
2420 this.filePopup_.contentWindow &&
2421 this.filePopup_.contentWindow.unload)
2422 this.filePopup_.contentWindow.unload(true /* exiting */);
2423 if (this.progressCenterPanel_)
2424 this.backgroundPage_.background.progressCenter.removePanel(
2425 this.progressCenterPanel_);
2426 if (this.fileOperationManager_) {
2427 if (this.onCopyProgressBound_) {
2428 this.fileOperationManager_.removeEventListener(
2429 'copy-progress', this.onCopyProgressBound_);
2431 if (this.onEntryChangedBound_) {
2432 this.fileOperationManager_.removeEventListener(
2433 'entry-changed', this.onEntryChangedBound_);
2436 window.closing = true;
2437 if (this.backgroundPage_)
2438 this.backgroundPage_.background.tryClose();
2441 FileManager.prototype.initiateRename = function() {
2442 var item = this.currentList_.ensureLeadItemExists();
2445 var label = item.querySelector('.filename-label');
2446 var input = this.renameInput_;
2448 input.value = label.textContent;
2449 item.setAttribute('renaming', '');
2450 label.parentNode.appendChild(input);
2452 var selectionEnd = input.value.lastIndexOf('.');
2453 if (selectionEnd == -1) {
2456 input.selectionStart = 0;
2457 input.selectionEnd = selectionEnd;
2460 // This has to be set late in the process so we don't handle spurious
2462 input.currentEntry = this.currentList_.dataModel.item(item.listIndex);
2463 this.table_.startBatchUpdates();
2464 this.grid_.startBatchUpdates();
2468 * @type {Event} Key event.
2471 FileManager.prototype.onRenameInputKeyDown_ = function(event) {
2472 if (!this.isRenamingInProgress())
2475 // Do not move selection or lead item in list during rename.
2476 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
2477 event.stopPropagation();
2480 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
2481 case 'U+001B': // Escape
2482 this.cancelRename_();
2483 event.preventDefault();
2487 this.commitRename_();
2488 event.preventDefault();
2494 * @type {Event} Blur event.
2497 FileManager.prototype.onRenameInputBlur_ = function(event) {
2498 if (this.isRenamingInProgress() && !this.renameInput_.validation_)
2499 this.commitRename_();
2505 FileManager.prototype.commitRename_ = function() {
2506 var input = this.renameInput_;
2507 var entry = input.currentEntry;
2508 var newName = input.value;
2510 if (newName == entry.name) {
2511 this.cancelRename_();
2515 var renamedItemElement = this.findListItemForNode_(this.renameInput_);
2516 var nameNode = renamedItemElement.querySelector('.filename-label');
2518 input.validation_ = true;
2519 var validationDone = function(valid) {
2520 input.validation_ = false;
2523 // Cancel rename if it fails to restore focus from alert dialog.
2524 // Otherwise, just cancel the commitment and continue to rename.
2525 if (this.document_.activeElement != input)
2526 this.cancelRename_();
2530 // Validation succeeded. Do renaming.
2531 this.renameInput_.currentEntry = null;
2532 if (this.renameInput_.parentNode)
2533 this.renameInput_.parentNode.removeChild(this.renameInput_);
2534 renamedItemElement.setAttribute('renaming', 'provisional');
2536 // Optimistically apply new name immediately to avoid flickering in
2538 nameNode.textContent = newName;
2542 function(newEntry) {
2543 this.directoryModel_.onRenameEntry(entry, newEntry);
2544 renamedItemElement.removeAttribute('renaming');
2545 this.table_.endBatchUpdates();
2546 this.grid_.endBatchUpdates();
2549 // Write back to the old name.
2550 nameNode.textContent = entry.name;
2551 renamedItemElement.removeAttribute('renaming');
2552 this.table_.endBatchUpdates();
2553 this.grid_.endBatchUpdates();
2555 // Show error dialog.
2557 if (error.name == util.FileError.PATH_EXISTS_ERR ||
2558 error.name == util.FileError.TYPE_MISMATCH_ERR) {
2559 // Check the existing entry is file or not.
2560 // 1) If the entry is a file:
2561 // a) If we get PATH_EXISTS_ERR, a file exists.
2562 // b) If we get TYPE_MISMATCH_ERR, a directory exists.
2563 // 2) If the entry is a directory:
2564 // a) If we get PATH_EXISTS_ERR, a directory exists.
2565 // b) If we get TYPE_MISMATCH_ERR, a file exists.
2567 (entry.isFile && error.name ==
2568 util.FileError.PATH_EXISTS_ERR) ||
2569 (!entry.isFile && error.name ==
2570 util.FileError.TYPE_MISMATCH_ERR) ?
2571 'FILE_ALREADY_EXISTS' :
2572 'DIRECTORY_ALREADY_EXISTS',
2575 message = strf('ERROR_RENAMING', entry.name,
2576 util.getFileErrorString(error.name));
2579 this.alert.show(message);
2583 // TODO(haruki): this.getCurrentDirectoryEntry() might not return the actual
2584 // parent if the directory content is a search result. Fix it to do proper
2586 this.validateFileName_(this.getCurrentDirectoryEntry(),
2588 validationDone.bind(this));
2594 FileManager.prototype.cancelRename_ = function() {
2595 this.renameInput_.currentEntry = null;
2597 var item = this.findListItemForNode_(this.renameInput_);
2599 item.removeAttribute('renaming');
2601 var parent = this.renameInput_.parentNode;
2603 parent.removeChild(this.renameInput_);
2605 this.table_.endBatchUpdates();
2606 this.grid_.endBatchUpdates();
2610 * @param {Event} Key event.
2613 FileManager.prototype.onFilenameInputInput_ = function() {
2614 this.selectionHandler_.updateOkButton();
2618 * @param {Event} Key event.
2621 FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
2622 if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */)
2623 this.okButton_.click();
2627 * @param {Event} Focus event.
2630 FileManager.prototype.onFilenameInputFocus_ = function(event) {
2631 var input = this.filenameInput_;
2633 // On focus we want to select everything but the extension, but
2634 // Chrome will select-all after the focus event completes. We
2635 // schedule a timeout to alter the focus after that happens.
2636 setTimeout(function() {
2637 var selectionEnd = input.value.lastIndexOf('.');
2638 if (selectionEnd == -1) {
2641 input.selectionStart = 0;
2642 input.selectionEnd = selectionEnd;
2650 FileManager.prototype.onScanStarted_ = function() {
2651 if (this.scanInProgress_) {
2652 this.table_.list.endBatchUpdates();
2653 this.grid_.endBatchUpdates();
2656 if (this.commandHandler)
2657 this.commandHandler.updateAvailability();
2658 this.table_.list.startBatchUpdates();
2659 this.grid_.startBatchUpdates();
2660 this.scanInProgress_ = true;
2662 this.scanUpdatedAtLeastOnceOrCompleted_ = false;
2663 if (this.scanCompletedTimer_) {
2664 clearTimeout(this.scanCompletedTimer_);
2665 this.scanCompletedTimer_ = null;
2668 if (this.scanUpdatedTimer_) {
2669 clearTimeout(this.scanUpdatedTimer_);
2670 this.scanUpdatedTimer_ = null;
2673 if (this.spinner_.hidden) {
2674 this.cancelSpinnerTimeout_();
2675 this.showSpinnerTimeout_ =
2676 setTimeout(this.showSpinner_.bind(this, true), 500);
2683 FileManager.prototype.onScanCompleted_ = function() {
2684 if (!this.scanInProgress_) {
2685 console.error('Scan-completed event recieved. But scan is not started.');
2689 if (this.commandHandler)
2690 this.commandHandler.updateAvailability();
2691 this.hideSpinnerLater_();
2693 if (this.scanUpdatedTimer_) {
2694 clearTimeout(this.scanUpdatedTimer_);
2695 this.scanUpdatedTimer_ = null;
2698 // To avoid flickering postpone updating the ui by a small amount of time.
2699 // There is a high chance, that metadata will be received within 50 ms.
2700 this.scanCompletedTimer_ = setTimeout(function() {
2701 // Check if batch updates are already finished by onScanUpdated_().
2702 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2703 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2704 this.updateMiddleBarVisibility_();
2707 this.scanInProgress_ = false;
2708 this.table_.list.endBatchUpdates();
2709 this.grid_.endBatchUpdates();
2710 this.scanCompletedTimer_ = null;
2717 FileManager.prototype.onScanUpdated_ = function() {
2718 if (!this.scanInProgress_) {
2719 console.error('Scan-updated event recieved. But scan is not started.');
2723 if (this.scanUpdatedTimer_ || this.scanCompletedTimer_)
2726 // Show contents incrementally by finishing batch updated, but only after
2727 // 200ms elapsed, to avoid flickering when it is not necessary.
2728 this.scanUpdatedTimer_ = setTimeout(function() {
2729 // We need to hide the spinner only once.
2730 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2731 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2732 this.hideSpinnerLater_();
2733 this.updateMiddleBarVisibility_();
2737 if (this.scanInProgress_) {
2738 this.table_.list.endBatchUpdates();
2739 this.grid_.endBatchUpdates();
2740 this.table_.list.startBatchUpdates();
2741 this.grid_.startBatchUpdates();
2743 this.scanUpdatedTimer_ = null;
2750 FileManager.prototype.onScanCancelled_ = function() {
2751 if (!this.scanInProgress_) {
2752 console.error('Scan-cancelled event recieved. But scan is not started.');
2756 if (this.commandHandler)
2757 this.commandHandler.updateAvailability();
2758 this.hideSpinnerLater_();
2759 if (this.scanCompletedTimer_) {
2760 clearTimeout(this.scanCompletedTimer_);
2761 this.scanCompletedTimer_ = null;
2763 if (this.scanUpdatedTimer_) {
2764 clearTimeout(this.scanUpdatedTimer_);
2765 this.scanUpdatedTimer_ = null;
2767 // Finish unfinished batch updates.
2768 if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
2769 this.scanUpdatedAtLeastOnceOrCompleted_ = true;
2770 this.updateMiddleBarVisibility_();
2773 this.scanInProgress_ = false;
2774 this.table_.list.endBatchUpdates();
2775 this.grid_.endBatchUpdates();
2779 * Handle the 'rescan-completed' from the DirectoryModel.
2782 FileManager.prototype.onRescanCompleted_ = function() {
2783 this.selectionHandler_.onFileSelectionChanged();
2789 FileManager.prototype.cancelSpinnerTimeout_ = function() {
2790 if (this.showSpinnerTimeout_) {
2791 clearTimeout(this.showSpinnerTimeout_);
2792 this.showSpinnerTimeout_ = null;
2799 FileManager.prototype.hideSpinnerLater_ = function() {
2800 this.cancelSpinnerTimeout_();
2801 this.showSpinner_(false);
2805 * @param {boolean} on True to show, false to hide.
2808 FileManager.prototype.showSpinner_ = function(on) {
2809 if (on && this.directoryModel_ && this.directoryModel_.isScanning())
2810 this.spinner_.hidden = false;
2812 if (!on && (!this.directoryModel_ ||
2813 !this.directoryModel_.isScanning() ||
2814 this.directoryModel_.getFileList().length != 0)) {
2815 this.spinner_.hidden = true;
2819 FileManager.prototype.createNewFolder = function() {
2820 var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
2822 // Find a name that doesn't exist in the data model.
2823 var files = this.directoryModel_.getFileList();
2825 for (var i = 0; i < files.length; i++) {
2826 var name = files.item(i).name;
2827 // Filtering names prevents from conflicts with prototype's names
2829 if (name.substring(0, defaultName.length) == defaultName)
2833 var baseName = defaultName;
2838 var advance = function() {
2844 var current = function() {
2845 return baseName + separator + index + suffix;
2848 // Accessing hasOwnProperty is safe since hash properties filtered.
2849 while (hash.hasOwnProperty(current())) {
2854 var list = self.currentList_;
2855 var tryCreate = function() {
2858 var onSuccess = function(entry) {
2859 metrics.recordUserAction('CreateNewFolder');
2860 list.selectedItem = entry;
2862 self.table_.list.endBatchUpdates();
2863 self.grid_.endBatchUpdates();
2865 self.initiateRename();
2868 var onError = function(error) {
2869 self.table_.list.endBatchUpdates();
2870 self.grid_.endBatchUpdates();
2872 self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
2873 util.getFileErrorString(error.name)));
2876 var onAbort = function() {
2877 self.table_.list.endBatchUpdates();
2878 self.grid_.endBatchUpdates();
2881 this.table_.list.startBatchUpdates();
2882 this.grid_.startBatchUpdates();
2883 this.directoryModel_.createDirectory(current(),
2890 * @param {Event} event Click event.
2893 FileManager.prototype.onDetailViewButtonClick_ = function(event) {
2894 // Stop propagate and hide the menu manually, in order to prevent the focus
2895 // from being back to the button. (cf. http://crbug.com/248479)
2896 event.stopPropagation();
2897 this.gearButton_.hideMenu();
2898 this.gearButton_.blur();
2899 this.setListType(FileManager.ListType.DETAIL);
2903 * @param {Event} event Click event.
2906 FileManager.prototype.onThumbnailViewButtonClick_ = function(event) {
2907 // Stop propagate and hide the menu manually, in order to prevent the focus
2908 // from being back to the button. (cf. http://crbug.com/248479)
2909 event.stopPropagation();
2910 this.gearButton_.hideMenu();
2911 this.gearButton_.blur();
2912 this.setListType(FileManager.ListType.THUMBNAIL);
2916 * KeyDown event handler for the document.
2917 * @param {Event} event Key event.
2920 FileManager.prototype.onKeyDown_ = function(event) {
2921 if (event.keyCode === 9) // Tab
2922 this.pressingTab_ = true;
2923 if (event.keyCode === 17) // Ctrl
2924 this.pressingCtrl_ = true;
2926 if (event.srcElement === this.renameInput_) {
2927 // Ignore keydown handler in the rename input box.
2931 switch (util.getKeyModifiers(event) + event.keyCode) {
2932 case 'Ctrl-190': // Ctrl-. => Toggle filter files.
2933 this.fileFilter_.setFilterHidden(
2934 !this.fileFilter_.isFilterHiddenOn());
2935 event.preventDefault();
2938 case '27': // Escape => Cancel dialog.
2939 if (this.dialogType != DialogType.FULL_PAGE) {
2940 // If there is nothing else for ESC to do, then cancel the dialog.
2941 event.preventDefault();
2942 this.cancelButton_.click();
2949 * KeyUp event handler for the document.
2950 * @param {Event} event Key event.
2953 FileManager.prototype.onKeyUp_ = function(event) {
2954 if (event.keyCode === 9) // Tab
2955 this.pressingTab_ = false;
2956 if (event.keyCode == 17) // Ctrl
2957 this.pressingCtrl_ = false;
2961 * KeyDown event handler for the div#list-container element.
2962 * @param {Event} event Key event.
2965 FileManager.prototype.onListKeyDown_ = function(event) {
2966 if (event.srcElement.tagName == 'INPUT') {
2967 // Ignore keydown handler in the rename input box.
2971 switch (util.getKeyModifiers(event) + event.keyCode) {
2972 case '8': // Backspace => Up one directory.
2973 event.preventDefault();
2974 // TODO(mtomasz): Use Entry.getParent() instead.
2975 if (!this.getCurrentDirectoryEntry())
2977 var currentEntry = this.getCurrentDirectoryEntry();
2978 var locationInfo = this.volumeManager_.getLocationInfo(currentEntry);
2979 // TODO(mtomasz): There may be a tiny race in here.
2980 if (locationInfo && !locationInfo.isRootEntry &&
2981 !locationInfo.isSpecialSearchRoot) {
2982 currentEntry.getParent(function(parentEntry) {
2983 this.directoryModel_.changeDirectoryEntry(parentEntry);
2984 }.bind(this), function() { /* Ignore errors. */});
2988 case '13': // Enter => Change directory or perform default action.
2989 // TODO(dgozman): move directory action to dispatchSelectionAction.
2990 var selection = this.getSelection();
2991 if (selection.totalCount == 1 &&
2992 selection.entries[0].isDirectory &&
2993 !DialogType.isFolderDialog(this.dialogType)) {
2994 event.preventDefault();
2995 this.onDirectoryAction_(selection.entries[0]);
2996 } else if (this.dispatchSelectionAction_()) {
2997 event.preventDefault();
3002 switch (event.keyIdentifier) {
3009 // When navigating with keyboard we hide the distracting mouse hover
3010 // highlighting until the user moves the mouse again.
3011 this.setNoHover_(true);
3017 * Suppress/restore hover highlighting in the list container.
3018 * @param {boolean} on True to temporarity hide hover state.
3021 FileManager.prototype.setNoHover_ = function(on) {
3023 this.listContainer_.classList.add('nohover');
3025 this.listContainer_.classList.remove('nohover');
3030 * KeyPress event handler for the div#list-container element.
3031 * @param {Event} event Key event.
3034 FileManager.prototype.onListKeyPress_ = function(event) {
3035 if (event.srcElement.tagName == 'INPUT') {
3036 // Ignore keypress handler in the rename input box.
3040 if (event.ctrlKey || event.metaKey || event.altKey)
3043 var now = new Date();
3044 var char = String.fromCharCode(event.charCode).toLowerCase();
3045 var text = now - this.textSearchState_.date > 1000 ? '' :
3046 this.textSearchState_.text;
3047 this.textSearchState_ = {text: text + char, date: now};
3049 this.doTextSearch_();
3053 * Mousemove event handler for the div#list-container element.
3054 * @param {Event} event Mouse event.
3057 FileManager.prototype.onListMouseMove_ = function(event) {
3058 // The user grabbed the mouse, restore the hover highlighting.
3059 this.setNoHover_(false);
3063 * Performs a 'text search' - selects a first list entry with name
3064 * starting with entered text (case-insensitive).
3067 FileManager.prototype.doTextSearch_ = function() {
3068 var text = this.textSearchState_.text;
3072 var dm = this.directoryModel_.getFileList();
3073 for (var index = 0; index < dm.length; ++index) {
3074 var name = dm.item(index).name;
3075 if (name.substring(0, text.length).toLowerCase() == text) {
3076 this.currentList_.selectionModel.selectedIndexes = [index];
3081 this.textSearchState_.text = '';
3085 * Handle a click of the cancel button. Closes the window.
3086 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3088 * @param {Event} event The click event.
3091 FileManager.prototype.onCancel_ = function(event) {
3092 chrome.fileBrowserPrivate.cancelDialog();
3097 * Resolves selected file urls returned from an Open dialog.
3099 * For drive files this involves some special treatment.
3100 * Starts getting drive files if needed.
3102 * @param {Array.<string>} fileUrls Drive URLs.
3103 * @param {function(Array.<string>)} callback To be called with fixed URLs.
3106 FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) {
3107 if (this.isOnDrive()) {
3108 chrome.fileBrowserPrivate.getDriveFiles(
3110 function(localPaths) {
3119 * Closes this modal dialog with some files selected.
3120 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
3121 * @param {Object} selection Contains urls, filterIndex and multiple fields.
3124 FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) {
3126 function callback() {
3129 if (selection.multiple) {
3130 chrome.fileBrowserPrivate.selectFiles(
3131 selection.urls, this.params_.shouldReturnLocalPath, callback);
3133 var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE);
3134 chrome.fileBrowserPrivate.selectFile(
3135 selection.urls[0], selection.filterIndex, forOpening,
3136 this.params_.shouldReturnLocalPath, callback);
3141 * Tries to close this modal dialog with some files selected.
3142 * Performs preprocessing if needed (e.g. for Drive).
3143 * @param {Object} selection Contains urls, filterIndex and multiple fields.
3146 FileManager.prototype.selectFilesAndClose_ = function(selection) {
3147 if (!this.isOnDrive() ||
3148 this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3149 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3153 var shade = this.document_.createElement('div');
3154 shade.className = 'shade';
3155 var footer = this.dialogDom_.querySelector('.button-panel');
3156 var progress = footer.querySelector('.progress-track');
3157 progress.style.width = '0%';
3158 var cancelled = false;
3160 var progressMap = {};
3161 var filesStarted = 0;
3162 var filesTotal = selection.urls.length;
3163 for (var index = 0; index < selection.urls.length; index++) {
3164 progressMap[selection.urls[index]] = -1;
3166 var lastPercent = 0;
3170 var onFileTransfersUpdated = function(statusList) {
3171 for (var index = 0; index < statusList.length; index++) {
3172 var status = statusList[index];
3173 var escaped = encodeURI(status.fileUrl);
3174 if (!(escaped in progressMap)) continue;
3175 if (status.total == -1) continue;
3177 var old = progressMap[escaped];
3179 // -1 means we don't know file size yet.
3180 bytesTotal += status.total;
3184 bytesDone += status.processed - old;
3185 progressMap[escaped] = status.processed;
3188 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
3189 // For files we don't have information about, assume the progress is zero.
3190 percent = percent * filesStarted / filesTotal * 100;
3191 // Do not decrease the progress. This may happen, if first downloaded
3192 // file is small, and the second one is large.
3193 lastPercent = Math.max(lastPercent, percent);
3194 progress.style.width = lastPercent + '%';
3197 var setup = function() {
3198 this.document_.querySelector('.dialog-container').appendChild(shade);
3199 setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100);
3200 footer.setAttribute('progress', 'progress');
3201 this.cancelButton_.removeEventListener('click', this.onCancelBound_);
3202 this.cancelButton_.addEventListener('click', onCancel);
3203 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
3204 onFileTransfersUpdated);
3207 var cleanup = function() {
3208 shade.parentNode.removeChild(shade);
3209 footer.removeAttribute('progress');
3210 this.cancelButton_.removeEventListener('click', onCancel);
3211 this.cancelButton_.addEventListener('click', this.onCancelBound_);
3212 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
3213 onFileTransfersUpdated);
3216 var onCancel = function() {
3218 // According to API cancel may fail, but there is no proper UI to reflect
3219 // this. So, we just silently assume that everything is cancelled.
3220 chrome.fileBrowserPrivate.cancelFileTransfers(
3221 selection.urls, function(response) {});
3225 var onResolved = function(resolvedUrls) {
3226 if (cancelled) return;
3228 selection.urls = resolvedUrls;
3229 // Call next method on a timeout, as it's unsafe to
3230 // close a window from a callback.
3231 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
3234 var onProperties = function(properties) {
3235 for (var i = 0; i < properties.length; i++) {
3236 if (!properties[i] || properties[i].present) {
3237 // For files already in GCache, we don't get any transfer updates.
3241 this.resolveSelectResults_(selection.urls, onResolved);
3246 // TODO(mtomasz): Use Entry instead of URLs, if possible.
3247 util.URLsToEntries(selection.urls, function(entries) {
3248 this.metadataCache_.get(entries, 'drive', onProperties);
3253 * Handle a click of the ok button.
3255 * The ok button has different UI labels depending on the type of dialog, but
3256 * in code it's always referred to as 'ok'.
3258 * @param {Event} event The click event.
3261 FileManager.prototype.onOk_ = function(event) {
3262 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
3263 // Save-as doesn't require a valid selection from the list, since
3264 // we're going to take the filename from the text input.
3265 var filename = this.filenameInput_.value;
3267 throw new Error('Missing filename!');
3269 var directory = this.getCurrentDirectoryEntry();
3270 this.validateFileName_(directory, filename, function(isValid) {
3274 if (util.isFakeEntry(directory)) {
3275 // Can't save a file into a fake directory.
3279 var selectFileAndClose = function() {
3280 // TODO(mtomasz): Clean this up by avoiding constructing a URL
3281 // via string concatenation.
3282 var currentDirUrl = directory.toURL();
3283 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
3284 currentDirUrl += '/';
3285 this.selectFilesAndClose_({
3286 urls: [currentDirUrl + encodeURIComponent(filename)],
3288 filterIndex: this.getSelectedFilterIndex_(filename)
3293 filename, {create: false},
3295 // An existing file is found. Show confirmation dialog to
3296 // overwrite it. If the user select "OK" on the dialog, save it.
3297 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
3298 selectFileAndClose);
3301 if (error.name == util.FileError.NOT_FOUND_ERR) {
3302 // The file does not exist, so it should be ok to create a
3304 selectFileAndClose();
3307 if (error.name == util.FileError.TYPE_MISMATCH_ERR) {
3308 // An directory is found.
3309 // Do not allow to overwrite directory.
3310 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
3314 // Unexpected error.
3315 console.error('File save failed: ' + error.code);
3322 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
3324 if (DialogType.isFolderDialog(this.dialogType) &&
3325 selectedIndexes.length == 0) {
3326 var url = this.getCurrentDirectoryEntry().toURL();
3327 var singleSelection = {
3330 filterIndex: this.getSelectedFilterIndex_()
3332 this.selectFilesAndClose_(singleSelection);
3336 // All other dialog types require at least one selected list item.
3337 // The logic to control whether or not the ok button is enabled should
3338 // prevent us from ever getting here, but we sanity check to be sure.
3339 if (!selectedIndexes.length)
3340 throw new Error('Nothing selected!');
3342 var dm = this.directoryModel_.getFileList();
3343 for (var i = 0; i < selectedIndexes.length; i++) {
3344 var entry = dm.item(selectedIndexes[i]);
3346 console.error('Error locating selected file at index: ' + i);
3350 files.push(entry.toURL());
3353 // Multi-file selection has no other restrictions.
3354 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
3355 var multipleSelection = {
3359 this.selectFilesAndClose_(multipleSelection);
3363 // Everything else must have exactly one.
3364 if (files.length > 1)
3365 throw new Error('Too many files selected!');
3367 var selectedEntry = dm.item(selectedIndexes[0]);
3369 if (DialogType.isFolderDialog(this.dialogType)) {
3370 if (!selectedEntry.isDirectory)
3371 throw new Error('Selected entry is not a folder!');
3372 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
3373 if (!selectedEntry.isFile)
3374 throw new Error('Selected entry is not a file!');
3377 var singleSelection = {
3380 filterIndex: this.getSelectedFilterIndex_()
3382 this.selectFilesAndClose_(singleSelection);
3386 * Verifies the user entered name for file or folder to be created or
3387 * renamed to. Name restrictions must correspond to File API restrictions
3388 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is
3389 * out of date (spec is
3390 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to
3391 * be fixed. Shows message box if the name is invalid.
3393 * It also verifies if the name length is in the limit of the filesystem.
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) {
3405 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
3407 msg = strf('ERROR_INVALID_CHARACTER', testResult[0]);
3408 } else if (/^\s*$/i.test(name)) {
3409 msg = str('ERROR_WHITESPACE_NAME');
3410 } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) {
3411 msg = str('ERROR_RESERVED_NAME');
3412 } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') {
3413 msg = str('ERROR_HIDDEN_NAME');
3417 this.alert.show(msg, function() {
3424 chrome.fileBrowserPrivate.validatePathNameLength(
3425 parentEntry.toURL(), name, function(valid) {
3427 self.alert.show(str('ERROR_LONG_NAME'),
3428 function() { onDone(false); });
3436 * Toggle whether mobile data is used for sync.
3438 FileManager.prototype.toggleDriveSyncSettings = function() {
3439 // If checked, the sync is disabled.
3440 var nowCellularDisabled = this.syncButton.hasAttribute('checked');
3441 var changeInfo = {cellularDisabled: !nowCellularDisabled};
3442 chrome.fileBrowserPrivate.setPreferences(changeInfo);
3446 * Toggle whether Google Docs files are shown.
3448 FileManager.prototype.toggleDriveHostedSettings = function() {
3449 // If checked, showing drive hosted files is enabled.
3450 var nowHostedFilesEnabled = this.hostedButton.hasAttribute('checked');
3451 var nowHostedFilesDisabled = !nowHostedFilesEnabled;
3453 var changeInfo = {hostedFilesDisabled: !nowHostedFilesDisabled};
3455 var changeInfo = {};
3456 changeInfo['hostedFilesDisabled'] = !nowHostedFilesDisabled;
3457 chrome.fileBrowserPrivate.setPreferences(changeInfo);
3461 * Invoked when the search box is changed.
3463 * @param {Event} event The changed event.
3466 FileManager.prototype.onSearchBoxUpdate_ = function(event) {
3467 var searchString = this.searchBox_.value;
3469 if (this.isOnDrive()) {
3470 // When the search text is changed, finishes the search and showes back
3471 // the last directory by passing an empty string to
3472 // {@code DirectoryModel.search()}.
3473 if (this.directoryModel_.isSearching() &&
3474 this.lastSearchQuery_ != searchString) {
3478 // On drive, incremental search is not invoked since we have an auto-
3479 // complete suggestion instead.
3483 this.search_(searchString);
3487 * Handle the search clear button click.
3490 FileManager.prototype.onSearchClearButtonClick_ = function() {
3491 this.ui_.searchBox.clear();
3492 this.onSearchBoxUpdate_();
3496 * Search files and update the list with the search result.
3498 * @param {string} searchString String to be searched with.
3501 FileManager.prototype.search_ = function(searchString) {
3502 var noResultsDiv = this.document_.getElementById('no-search-results');
3504 var reportEmptySearchResults = function() {
3505 if (this.directoryModel_.getFileList().length === 0) {
3506 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
3507 // hence we escapes |searchString| here.
3508 var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
3509 util.htmlEscape(searchString));
3510 noResultsDiv.innerHTML = html;
3511 noResultsDiv.setAttribute('show', 'true');
3513 noResultsDiv.removeAttribute('show');
3517 var hideNoResultsDiv = function() {
3518 noResultsDiv.removeAttribute('show');
3521 this.doSearch(searchString,
3522 reportEmptySearchResults.bind(this),
3523 hideNoResultsDiv.bind(this));
3527 * Performs search and displays results.
3529 * @param {string} query Query that will be searched for.
3530 * @param {function()=} opt_onSearchRescan Function that will be called when
3531 * the search directory is rescanned (i.e. search results are displayed).
3532 * @param {function()=} opt_onClearSearch Function to be called when search
3533 * state gets cleared.
3535 FileManager.prototype.doSearch = function(
3536 searchString, opt_onSearchRescan, opt_onClearSearch) {
3537 var onSearchRescan = opt_onSearchRescan || function() {};
3538 var onClearSearch = opt_onClearSearch || function() {};
3540 this.lastSearchQuery_ = searchString;
3541 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
3545 * Requests autocomplete suggestions for files on Drive.
3546 * Once the suggestions are returned, the autocomplete popup will show up.
3548 * @param {string} query The text to autocomplete from.
3551 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
3552 query = query.trimLeft();
3554 // Only Drive supports auto-compelete
3555 if (!this.isOnDrive())
3558 // Remember the most recent query. If there is an other request in progress,
3559 // then it's result will be discarded and it will call a new request for
3561 this.lastAutocompleteQuery_ = query;
3562 if (this.autocompleteSuggestionsBusy_)
3565 // The autocomplete list should be resized and repositioned here as the
3566 // search box is resized when it's focused.
3567 this.autocompleteList_.syncWidthAndPositionToInput();
3570 this.autocompleteList_.suggestions = [];
3574 var headerItem = {isHeaderItem: true, searchQuery: query};
3575 if (!this.autocompleteList_.dataModel ||
3576 this.autocompleteList_.dataModel.length == 0)
3577 this.autocompleteList_.suggestions = [headerItem];
3579 // Updates only the head item to prevent a flickering on typing.
3580 this.autocompleteList_.dataModel.splice(0, 1, headerItem);
3582 this.autocompleteSuggestionsBusy_ = true;
3584 var searchParams = {
3589 chrome.fileBrowserPrivate.searchDriveMetadata(
3591 function(suggestions) {
3592 this.autocompleteSuggestionsBusy_ = false;
3594 // Discard results for previous requests and fire a new search
3595 // for the most recent query.
3596 if (query != this.lastAutocompleteQuery_) {
3597 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
3601 // Keeps the items in the suggestion list.
3602 this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
3607 * Opens the currently selected suggestion item.
3610 FileManager.prototype.openAutocompleteSuggestion_ = function() {
3611 var selectedItem = this.autocompleteList_.selectedItem;
3613 // If the entry is the search item or no entry is selected, just change to
3614 // the search result.
3615 if (!selectedItem || selectedItem.isHeaderItem) {
3616 var query = selectedItem ?
3617 selectedItem.searchQuery : this.searchBox_.value;
3618 this.search_(query);
3622 var entry = selectedItem.entry;
3623 // If the entry is a directory, just change the directory.
3624 if (entry.isDirectory) {
3625 this.onDirectoryAction_(entry);
3629 var entries = [entry];
3632 // To open a file, first get the mime type.
3633 this.metadataCache_.get(entries, 'drive', function(props) {
3634 var mimeType = props[0].contentMimeType || '';
3635 var mimeTypes = [mimeType];
3636 var openIt = function() {
3637 if (self.dialogType == DialogType.FULL_PAGE) {
3638 var tasks = new FileTasks(self);
3639 tasks.init(entries, mimeTypes);
3640 tasks.executeDefault();
3646 // Change the current directory to the directory that contains the
3647 // selected file. Note that this is necessary for an image or a video,
3648 // which should be opened in the gallery mode, as the gallery mode
3649 // requires the entry to be in the current directory model. For
3650 // consistency, the current directory is always changed regardless of
3652 entry.getParent(function(parentEntry) {
3653 var onDirectoryChanged = function(event) {
3654 self.directoryModel_.removeEventListener('scan-completed',
3655 onDirectoryChanged);
3656 self.directoryModel_.selectEntry(entry);
3659 // changeDirectoryEntry() returns immediately. We should wait until the
3660 // directory scan is complete.
3661 self.directoryModel_.addEventListener('scan-completed',
3662 onDirectoryChanged);
3663 self.directoryModel_.changeDirectoryEntry(
3666 // Remove the listner if the change directory failed.
3667 self.directoryModel_.removeEventListener('scan-completed',
3668 onDirectoryChanged);
3674 FileManager.prototype.decorateSplitter = function(splitterElement) {
3677 var Splitter = cr.ui.Splitter;
3679 var customSplitter = cr.ui.define('div');
3681 customSplitter.prototype = {
3682 __proto__: Splitter.prototype,
3684 handleSplitterDragStart: function(e) {
3685 Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
3686 this.ownerDocument.documentElement.classList.add('col-resize');
3689 handleSplitterDragMove: function(deltaX) {
3690 Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
3694 handleSplitterDragEnd: function(e) {
3695 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
3696 this.ownerDocument.documentElement.classList.remove('col-resize');
3700 customSplitter.decorate(splitterElement);
3704 * Updates default action menu item to match passed taskItem (icon,
3705 * label and action).
3707 * @param {Object} defaultItem - taskItem to match.
3708 * @param {boolean} isMultiple - if multiple tasks available.
3710 FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
3713 if (defaultItem.iconType) {
3714 this.defaultActionMenuItem_.style.backgroundImage = '';
3715 this.defaultActionMenuItem_.setAttribute('file-type-icon',
3716 defaultItem.iconType);
3717 } else if (defaultItem.iconUrl) {
3718 this.defaultActionMenuItem_.style.backgroundImage =
3719 'url(' + defaultItem.iconUrl + ')';
3721 this.defaultActionMenuItem_.style.backgroundImage = '';
3724 this.defaultActionMenuItem_.label = defaultItem.title;
3725 this.defaultActionMenuItem_.disabled = !!defaultItem.disabled;
3726 this.defaultActionMenuItem_.taskId = defaultItem.taskId;
3729 var defaultActionSeparator =
3730 this.dialogDom_.querySelector('#default-action-separator');
3732 this.openWithCommand_.canExecuteChange();
3733 this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
3734 this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled;
3736 this.defaultActionMenuItem_.hidden = !defaultItem;
3737 defaultActionSeparator.hidden = !defaultItem;
3741 * @return {FileSelection} Selection object.
3743 FileManager.prototype.getSelection = function() {
3744 return this.selectionHandler_.selection;
3748 * @return {ArrayDataModel} File list.
3750 FileManager.prototype.getFileList = function() {
3751 return this.directoryModel_.getFileList();
3755 * @return {cr.ui.List} Current list object.
3757 FileManager.prototype.getCurrentList = function() {
3758 return this.currentList_;
3762 * Retrieve the preferences of the files.app. This method caches the result
3763 * and returns it unless opt_update is true.
3764 * @param {function(Object.<string, *>)} callback Callback to get the
3766 * @param {boolean=} opt_update If is's true, don't use the cache and
3767 * retrieve latest preference. Default is false.
3770 FileManager.prototype.getPreferences_ = function(callback, opt_update) {
3771 if (!opt_update && this.preferences_ !== undefined) {
3772 callback(this.preferences_);
3776 chrome.fileBrowserPrivate.getPreferences(function(prefs) {
3777 this.preferences_ = prefs;