blob: 9ef23dc9226acc8b110397e2fde9fa502c339d72 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* This variable is checked in SelectFileDialogExtensionBrowserTest.
* @type {number}
*/
window.JSErrorCount = 0;
/**
* Count uncaught exceptions.
*/
window.onerror = function() { window.JSErrorCount++; };
/**
* FileManager constructor.
*
* FileManager objects encapsulate the functionality of the file selector
* dialogs, as well as the full screen file manager application (though the
* latter is not yet implemented).
*
* @constructor
*/
function FileManager() {
this.initializeQueue_ = new AsyncUtil.Group();
}
/**
* Maximum delay in milliseconds for updating thumbnails in the bottom panel
* to mitigate flickering. If images load faster then the delay they replace
* old images smoothly. On the other hand we don't want to keep old images
* too long.
*
* @type {number}
* @const
*/
FileManager.THUMBNAIL_SHOW_DELAY = 100;
FileManager.prototype = {
__proto__: cr.EventTarget.prototype
};
/**
* Unload the file manager.
* Used by background.js (when running in the packaged mode).
*/
function unload() {
fileManager.onBeforeUnload_();
fileManager.onUnload_();
}
/**
* List of dialog types.
*
* Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
* FULL_PAGE which is specific to this code.
*
* @enum {string}
*/
var DialogType = {
SELECT_FOLDER: 'folder',
SELECT_UPLOAD_FOLDER: 'upload-folder',
SELECT_SAVEAS_FILE: 'saveas-file',
SELECT_OPEN_FILE: 'open-file',
SELECT_OPEN_MULTI_FILE: 'open-multi-file',
FULL_PAGE: 'full-page'
};
/**
* TextMeasure constructor.
*
* TextMeasure is a measure for text that returns the width of text. This
* class has a dummy span element. When measuring the width of text, it sets
* the text to the element and obtains the element's size by
* getBoundingClientRect.
*
* @constructor
* @param {HTMLElement} element Element that has styles of measured text. The
* width of text is mesures like as it is rendered in this element.
*/
var TextMeasure = function(element) {
var doc = element.ownerDocument;
this.dummySpan_ = doc.createElement('span');
this.dummySpan_ = doc.getElementsByTagName('body')[0].
appendChild(this.dummySpan_);
this.dummySpan_.style.position = 'absolute';
this.dummySpan_.style.visibility = 'hidden';
var styles = window.getComputedStyle(element, '');
var stylesToBeCopied = [
'fontSize',
'fontStyle',
'fontWeight',
'fontFamily',
'letterSpacing'
];
for (var i = 0; i < stylesToBeCopied.length; i++) {
this.dummySpan_.style[stylesToBeCopied[i]] = styles[stylesToBeCopied[i]];
}
};
/**
* Measures the widht of text.
*
* @param {string} text Text that is measured the width.
* @return {number} Width of the specified text.
*/
TextMeasure.prototype.getWidth = function(text) {
this.dummySpan_.innerText = text;
var rect = this.dummySpan_.getBoundingClientRect();
return rect ? rect.width : 0;
};
/**
* @param {string} type Dialog type.
* @return {boolean} Whether the type is modal.
*/
DialogType.isModal = function(type) {
return type == DialogType.SELECT_FOLDER ||
type == DialogType.SELECT_UPLOAD_FOLDER ||
type == DialogType.SELECT_SAVEAS_FILE ||
type == DialogType.SELECT_OPEN_FILE ||
type == DialogType.SELECT_OPEN_MULTI_FILE;
};
/**
* Bottom magrin of the list and tree for transparent preview panel.
* @const
*/
var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
// Anonymous "namespace".
(function() {
// Private variables and helper functions.
/**
* Location of the page to buy more storage for Google Drive.
*/
FileManager.GOOGLE_DRIVE_BUY_STORAGE =
'https://www.google.com/settings/storage';
/**
* Location of Google Drive specific help.
*/
FileManager.GOOGLE_DRIVE_HELP =
'https://support.google.com/chromeos/?p=filemanager_drivehelp';
/**
* Location of Google Drive specific help.
*/
FileManager.GOOGLE_DRIVE_ROOT = 'https://drive.google.com';
/**
* Location of Files App specific help.
*/
FileManager.FILES_APP_HELP =
'https://support.google.com/chromeos/?p=gsg_files_app';
/**
* Number of milliseconds in a day.
*/
var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
/**
* Some UI elements react on a single click and standard double click handling
* leads to confusing results. We ignore a second click if it comes soon
* after the first.
*/
var DOUBLE_CLICK_TIMEOUT = 200;
var removeChildren = function(element) {
element.textContent = '';
};
/**
* Update the elemenst to display the information about remainig space for
* the storage.
* @param {!Element} spaceInnerBar Block element for a percentage bar
* representing the remaining space.
* @param {!Element} spaceInfoLabel Inline element to contain the message.
* @param {!Element} spaceOuterBar Block element around the percentage bar.
*/
var updateSpaceInfo = function(
sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
spaceInnerBar.removeAttribute('pending');
if (sizeStatsResult) {
var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
var usedSpace =
sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
spaceInnerBar.style.width =
(100 * usedSpace / sizeStatsResult.totalSize) + '%';
spaceOuterBar.hidden = false;
} else {
spaceOuterBar.hidden = true;
spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
}
};
// Public statics.
FileManager.ListType = {
DETAIL: 'detail',
THUMBNAIL: 'thumb'
};
FileManager.prototype.initPreferences_ = function(callback) {
var group = new AsyncUtil.Group();
// DRIVE preferences should be initialized before creating DirectoryModel
// to rebuild the roots list.
group.add(this.getPreferences_.bind(this));
// Get startup preferences.
this.viewOptions_ = {};
group.add(function(done) {
this.dialogType = this.params_.type || DialogType.FULL_PAGE;
this.startupPrefName_ = 'file-manager-' + this.dialogType;
util.platform.getPreference(this.startupPrefName_, function(value) {
// Load the global default options.
try {
this.viewOptions_ = JSON.parse(value);
} catch (ignore) {}
// Override with window-specific options.
if (window.appState && window.appState.viewOptions) {
for (var key in window.appState.viewOptions) {
if (window.appState.viewOptions.hasOwnProperty(key))
this.viewOptions_[key] = window.appState.viewOptions[key];
}
}
done();
}.bind(this));
}.bind(this));
// Get the command line option.
group.add(function(done) {
chrome.commandLinePrivate.hasSwitch(
'file-manager-show-checkboxes', function(flag) {
this.showCheckboxes_ = flag;
done();
}.bind(this));
}.bind(this));
// Removes the user data which is no longer used.
// TODO(yoshiki): Remove this in M31 http://crbug.com/268784/
chrome.storage.local.remove('folder-shortcuts-list');
group.run(callback);
};
/**
* Request local file system, resolve roots and init_ after that.
* Warning, you can't use DOM nor any external scripts here, since it may not
* be loaded yet. Functions in util.* and metrics.* are available and can
* be used.
*
* @param {function()} callback Completion callback.
* @private
*/
FileManager.prototype.initFileSystem_ = function(callback) {
util.installFileErrorToString();
metrics.startInterval('Load.FileSystem');
chrome.fileBrowserPrivate.requestFileSystem(function(filesystem) {
metrics.recordInterval('Load.FileSystem');
this.filesystem_ = filesystem;
callback();
}.bind(this));
// Mount Drive if enabled.
if (this.isDriveEnabled())
this.volumeManager_.mountDrive(function() {}, function() {});
};
/**
* One time initialization for the file system and related things.
*
* @param {function()} callback Completion callback.
* @private
*/
FileManager.prototype.initFileSystemUI_ = function(callback) {
this.table_.startBatchUpdates();
this.grid_.startBatchUpdates();
this.initFileList_();
this.setupCurrentDirectory_(true /* page loading */);
// PyAuto tests monitor this state by polling this variable
this.__defineGetter__('workerInitialized_', function() {
return this.metadataCache_.isInitialized();
}.bind(this));
this.initDateTimeFormatters_();
var self = this;
// Get the 'allowRedeemOffers' preference before launching
// FileListBannerController.
this.getPreferences_(function(pref) {
/** @type {boolean} */
var showOffers = pref['allowRedeemOffers'];
self.bannersController_ = new FileListBannerController(
self.directoryModel_, self.volumeManager_, self.document_,
showOffers);
self.bannersController_.addEventListener('relayout',
self.onResize_.bind(self));
});
var dm = this.directoryModel_;
dm.addEventListener('directory-changed',
this.onDirectoryChanged_.bind(this));
dm.addEventListener('begin-update-files', function() {
self.currentList_.startBatchUpdates();
});
dm.addEventListener('end-update-files', function() {
self.restoreItemBeingRenamed_();
self.currentList_.endBatchUpdates();
});
dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
dm.addEventListener('rescan-completed',
this.refreshCurrentDirectoryMetadata_.bind(this));
/**
* If |item| in |parentView| is behind the preview panel, scrolls up the
* parent view and make the item visible.
*
* @param {HTMLElement} item Item to be visible in the parent.
* @param {HTMLElement} parentView View contains |selectedItem|.
*/
var ensureItemNotBehindPreviewPanel = function(item, parentView) {
var itemRect = item.getBoundingClientRect();
if (!itemRect)
return;
var itemBottom = itemRect.bottom;
var previewPanel = this.dialogDom_.querySelector('.preview-panel');
var previewPanelRects = previewPanel.getBoundingClientRect();
var panelHeight = previewPanelRects ? previewPanelRects.height : 0;
var listRect = parentView.getBoundingClientRect();
if (!listRect)
return;
var listBottom = listRect.bottom - panelHeight;
if (itemBottom > listBottom) {
var scrollOffset = itemBottom - listBottom;
parentView.scrollTop += scrollOffset;
}
}.bind(this);
var sm = this.directoryModel_.getFileListSelection();
sm.addEventListener('change', function() {
if (sm.selectedIndexes.length != 1)
return;
var view = (this.listType_ == FileManager.ListType.DETAIL) ?
this.table_.list : this.grid_;
var selectedItem = view.getListItemByIndex(sm.selectedIndex);
if (!selectedItem)
return;
ensureItemNotBehindPreviewPanel(selectedItem, view);
}.bind(this));
this.directoryTree_.addEventListener('change', function() {
var selectedSubTree = this.directoryTree_.selectedItem;
if (!selectedSubTree)
return;
var selectedItem = selectedSubTree.rowElement;
ensureItemNotBehindPreviewPanel(selectedItem, this.directoryTree_);
}.bind(this));
var stateChangeHandler =
this.onPreferencesChanged_.bind(this);
chrome.fileBrowserPrivate.onPreferencesChanged.addListener(
stateChangeHandler);
stateChangeHandler();
var driveConnectionChangedHandler =
this.onDriveConnectionChanged_.bind(this);
this.volumeManager_.addEventListener('drive-connection-changed',
driveConnectionChangedHandler);
driveConnectionChangedHandler();
// Set the initial focus and set it as a fallback.
this.document_.addEventListener('focusout', function(e) {
if (!e.relatedTarget)
setTimeout(this.refocus.bind(this), 0);
}.bind(this));
this.refocus();
this.initDataTransferOperations_();
this.initContextMenus_();
this.initCommands_();
this.updateFileTypeFilter_();
this.selectionHandler_.onFileSelectionChanged();
this.table_.endBatchUpdates();
this.grid_.endBatchUpdates();
callback();
};
/**
* @private
*/
FileManager.prototype.initDateTimeFormatters_ = function() {
var use12hourClock = !this.preferences_['use24hourClock'];
this.table_.setDateTimeFormat(use12hourClock);
};
/**
* @private
*/
FileManager.prototype.initDataTransferOperations_ = function() {
this.copyManager_ = new FileCopyManagerWrapper.getInstance();
this.butterBar_ = new ButterBar(this.dialogDom_, this.copyManager_);
// CopyManager and ButterBar are required for 'Delete' operation in
// Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
if (this.dialogType != DialogType.FULL_PAGE) return;
// TODO(hidehiko): Extract FileCopyManager related code from FileManager
// to simplify it.
this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
this.copyManager_.addEventListener(
'copy-progress', this.onCopyProgressBound_);
this.onCopyManagerEntryChangedBound_ =
this.onCopyManagerEntryChanged_.bind(this);
this.copyManager_.addEventListener(
'entry-changed', this.onCopyManagerEntryChangedBound_);
var controller = this.fileTransferController_ =
new FileTransferController(this.document_,
this.copyManager_,
this.metadataCache_,
this.directoryModel_);
controller.attachDragSource(this.table_.list);
controller.attachFileListDropTarget(this.table_.list);
controller.attachDragSource(this.grid_);
controller.attachFileListDropTarget(this.grid_);
controller.attachTreeDropTarget(this.directoryTree_);
controller.attachNavigationListDropTarget(this.navigationList_, true);
controller.attachCopyPasteHandlers();
controller.addEventListener('selection-copied',
this.blinkSelection.bind(this));
controller.addEventListener('selection-cut',
this.blinkSelection.bind(this));
};
/**
* One-time initialization of context menus.
* @private
*/
FileManager.prototype.initContextMenus_ = function() {
this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
cr.ui.Menu.decorate(this.fileContextMenu_);
cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
this.fileContextMenu_);
cr.ui.contextMenuHandler.setContextMenu(
this.document_.querySelector('.drive-welcome.page'),
this.fileContextMenu_);
this.rootsContextMenu_ =
this.dialogDom_.querySelector('#roots-context-menu');
cr.ui.Menu.decorate(this.rootsContextMenu_);
this.navigationList_.setContextMenu(this.rootsContextMenu_);
this.directoryTreeContextMenu_ =
this.dialogDom_.querySelector('#directory-tree-context-menu');
cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
this.textContextMenu_ =
this.dialogDom_.querySelector('#text-context-menu');
cr.ui.Menu.decorate(this.textContextMenu_);
this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
this.gearButton_.addEventListener('menushow',
this.refreshRemainingSpace_.bind(this,
false /* Without loading caption. */));
this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
'menuitem, hr';
cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
if (this.dialogType == DialogType.FULL_PAGE) {
var maximizeButton = this.dialogDom_.querySelector('#maximize-button');
maximizeButton.addEventListener('click', this.onMaximize.bind(this));
var closeButton = this.dialogDom_.querySelector('#close-button');
closeButton.addEventListener('click', this.onClose.bind(this));
}
this.syncButton.checkable = true;
this.hostedButton.checkable = true;
this.detailViewButton_.checkable = true;
this.thumbnailViewButton_.checkable = true;
if (util.platform.runningInBrowser()) {
// Supresses the default context menu.
this.dialogDom_.addEventListener('contextmenu', function(e) {
e.preventDefault();
e.stopPropagation();
});
}
};
FileManager.prototype.onMaximize = function() {
// Do not maximize when running via chrome://files in a browser.
if (util.platform.runningInBrowser())
return;
var appWindow = chrome.app.window.current();
if (appWindow.isMaximized())
appWindow.restore();
else
appWindow.maximize();
};
FileManager.prototype.onClose = function() {
// Do not close when running via chrome://files in a browser.
if (util.platform.runningInBrowser())
return;
window.close();
};
/**
* One-time initialization of commands.
* @private
*/
FileManager.prototype.initCommands_ = function() {
var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
for (var j = 0; j < commandButtons.length; j++)
CommandButton.decorate(commandButtons[j]);
// TODO(dzvorygin): Here we use this hack, since 'hidden' is standard
// attribute and we can't use it's setter as usual.
cr.ui.Command.prototype.setHidden = function(value) {
this.__lookupSetter__('hidden').call(this, value);
};
var commands = this.dialogDom_.querySelectorAll('command');
for (var i = 0; i < commands.length; i++)
cr.ui.Command.decorate(commands[i]);
var doc = this.document_;
CommandUtil.registerCommand(this.dialogContainer_, 'newfolder',
Commands.newFolderCommand, this, this.directoryModel_);
CommandUtil.registerCommand(this.dialogContainer_, 'newwindow',
Commands.newWindowCommand, this, this.directoryModel_);
CommandUtil.registerCommand(this.dialogContainer_, 'change-default-app',
Commands.changeDefaultAppCommand, this);
CommandUtil.registerCommand(this.navigationList_, 'unmount',
Commands.unmountCommand, this);
CommandUtil.registerCommand(this.navigationList_, 'import-photos',
Commands.importCommand, this.navigationList_);
CommandUtil.registerCommand(this.dialogContainer_, 'format',
Commands.formatCommand, this,
this.directoryModel_);
CommandUtil.registerCommand(this.dialogContainer_, 'delete',
Commands.deleteFileCommand, this);
CommandUtil.registerCommand(this.dialogContainer_, 'rename',
Commands.renameFileCommand, this);
CommandUtil.registerCommand(this.dialogContainer_, 'volume-help',
Commands.volumeHelpCommand, this);
CommandUtil.registerCommand(this.dialogContainer_, 'drive-buy-more-space',
Commands.driveBuySpaceCommand, this);
CommandUtil.registerCommand(this.dialogContainer_,
'drive-clear-local-cache', Commands.driveClearCacheCommand, this);
CommandUtil.registerCommand(this.dialogContainer_, 'drive-go-to-drive',
Commands.driveGoToDriveCommand, this);
CommandUtil.registerCommand(this.dialogContainer_, 'paste',
Commands.pasteFileCommand, doc, this.fileTransferController_);
CommandUtil.registerCommand(this.dialogContainer_, 'open-with',
Commands.openWithCommand, this);
CommandUtil.registerCommand(this.dialogContainer_, 'toggle-pinned',
Commands.togglePinnedCommand, this);
CommandUtil.registerCommand(this.dialogContainer_, 'zip-selection',
Commands.zipSelectionCommand, this, this.directoryModel_);
CommandUtil.registerCommand(this.dialogContainer_, 'share',
Commands.shareCommand, this);
CommandUtil.registerCommand(this.dialogContainer_,
'create-folder-shortcut', Commands.createFolderShortcutCommand, this);
CommandUtil.registerCommand(this.dialogContainer_,
'remove-folder-shortcut', Commands.removeFolderShortcutCommand, this);
CommandUtil.registerCommand(this.dialogContainer_, 'search',
Commands.searchCommand, this,
this.dialogDom_.querySelector('#search-box'));
// Register commands with CTRL-1..9 shortcuts for switching between
// volumes.
for (var i = 1; i <= 9; i++) {
CommandUtil.registerCommand(this.dialogContainer_,
'volume-switch-' + i,
Commands.volumeSwitchCommand,
this.navigationList_,
i);
}
CommandUtil.registerCommand(doc, 'zoom-in', Commands.zoomInCommand);
CommandUtil.registerCommand(doc, 'zoom-out', Commands.zoomOutCommand);
CommandUtil.registerCommand(doc, 'zoom-reset', Commands.zoomResetCommand);
CommandUtil.registerCommand(this.dialogContainer_, 'cut',
Commands.defaultCommand, doc);
CommandUtil.registerCommand(this.dialogContainer_, 'copy',
Commands.defaultCommand, doc);
var inputs = this.dialogDom_.querySelectorAll(
'input[type=text], input[type=search], textarea');
for (i = 0; i < inputs.length; i++) {
cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
this.registerInputCommands_(inputs[i]);
}
cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
this.textContextMenu_);
this.registerInputCommands_(this.renameInput_);
doc.addEventListener('command', this.setNoHover_.bind(this, true));
};
/**
* Registers cut, copy, paste and delete commands on input element.
*
* @param {Node} node Text input element to register on.
* @private
*/
FileManager.prototype.registerInputCommands_ = function(node) {
var defaultCommand = Commands.defaultCommand;
CommandUtil.forceDefaultHandler(node, 'cut');
CommandUtil.forceDefaultHandler(node, 'copy');
CommandUtil.forceDefaultHandler(node, 'paste');
CommandUtil.forceDefaultHandler(node, 'delete');
node.addEventListener('keydown', function(e) {
if (util.getKeyModifiers(e) + e.keyCode == '191') {
// If this key event is propagated, this is handled search command,
// which calls 'preventDefault' mehtod.
e.stopPropagation();
}
});
};
FileManager.prototype.initializeCore = function() {
this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
this.initializeQueue_.add(this.initStrings_.bind(this), [], 'initStrings');
this.initializeQueue_.add(
this.initPreferences_.bind(this), [], 'initPreferences');
this.initializeQueue_.add(
this.initFileSystem_.bind(this),
['initGeneral', 'initPreferences'], 'initFileSystem');
this.initializeQueue_.run();
};
FileManager.prototype.initializeUI = function(dialogDom, callback) {
this.dialogDom_ = dialogDom;
this.initializeQueue_.add(
this.initEssentialUI_.bind(this),
['initGeneral', 'initStrings'],
'initEssentialUI');
this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
['initEssentialUI'], 'initAdditionalUI');
this.initializeQueue_.add(
this.initFileSystemUI_.bind(this),
['initFileSystem', 'initAdditionalUI'],
'initFileSystemUI');
// Run again just in case if all pending closures have completed and the
// queue has stopped and monitor the completion.
this.initializeQueue_.run(callback);
};
/**
* Initializes general purpose basic things, which are used by other
* initializing methods.
*
* @param {function()} callback Completion callback.
* @private
*/
FileManager.prototype.initGeneral_ = function(callback) {
this.volumeManager_ = VolumeManager.getInstance();
if (window.appState) {
this.params_ = window.appState.params || {};
this.defaultPath = window.appState.defaultPath;
} else {
this.params_ = location.search ?
JSON.parse(decodeURIComponent(location.search.substr(1))) :
{};
this.defaultPath = this.params_.defaultPath;
}
callback();
};
/**
* One time initialization of strings (mostly i18n).
*
* @param {function()} callback Completion callback.
* @private
*/
FileManager.prototype.initStrings_ = function(callback) {
// Fetch the strings via the private api if running in the browser window.
// Otherwise, read cached strings from the local storage.
if (util.platform.runningInBrowser()) {
chrome.fileBrowserPrivate.getStrings(function(strings) {
loadTimeData.data = strings;
callback();
});
} else {
chrome.storage.local.get('strings', function(items) {
loadTimeData.data = items['strings'];
callback();
});
}
};
/**
* One time initialization of the Files.app's essential UI elements. These
* elements will be shown to the user. Only visible elements should be
* initialized here. Any heavy operation should be avoided. Files.app's
* window is shown at the end of this routine.
*
* @param {function()} callback Completion callback.
* @private
*/
FileManager.prototype.initEssentialUI_ = function(callback) {
this.listType_ = null;
this.filesystemObserverId_ = null;
this.driveObserverId_ = null;
this.document_ = this.dialogDom_.ownerDocument;
this.dialogType = this.params_.type || DialogType.FULL_PAGE;
this.startupPrefName_ = 'file-manager-' + this.dialogType;
// Used to filter out focusing by mouse.
this.suppressFocus_ = false;
// Optional list of file types.
this.fileTypes_ = this.params_.typeList || [];
metrics.recordEnum('Create', this.dialogType,
[DialogType.SELECT_FOLDER,
DialogType.SELECT_UPLOAD_FOLDER,
DialogType.SELECT_SAVEAS_FILE,
DialogType.SELECT_OPEN_FILE,
DialogType.SELECT_OPEN_MULTI_FILE,
DialogType.FULL_PAGE]);
this.selectionHandler_ = null;
this.ctrlKeyPressed_ = false;
this.metadataCache_ = MetadataCache.createFull();
this.hasFooterPanel_ =
this.dialogType == DialogType.SELECT_SAVEAS_FILE ||
this.dialogType == DialogType.SELECT_FOLDER;
// If the footer panel exists, the buttons are placed there. Otherwise,
// the buttons are on the preview panel.
var parentPanelOfButtons = this.dialogDom_.querySelector(
!this.hasFooterPanel_ ? '.preview-panel' : '.dialog-footer');
parentPanelOfButtons.classList.add('button-panel');
this.fileTypeSelector_ = parentPanelOfButtons.querySelector('.file-type');
this.okButton_ = parentPanelOfButtons.querySelector('.ok');
this.cancelButton_ = parentPanelOfButtons.querySelector('.cancel');
// Pre-populate the static localized strings.
i18nTemplate.process(this.document_, loadTimeData);
// Initialize the header.
this.dialogDom_.querySelector('#app-name').innerText =
chrome.runtime.getManifest().name;
this.initDialogType_();
// Show the window as soon as the UI pre-initialization is done.
if (this.dialogType == DialogType.FULL_PAGE &&
!util.platform.runningInBrowser()) {
chrome.app.window.current().show();
setTimeout(callback, 100); // Wait until the animation is finished.
} else {
callback();
}
};
/**
* One-time initialization of dialogs.
* @private
*/
FileManager.prototype.initDialogs_ = function() {
var d = cr.ui.dialogs;
d.BaseDialog.OK_LABEL = str('OK_LABEL');
d.BaseDialog.CANCEL_LABEL = str('CANCEL_LABEL');
this.error = new ErrorDialog(this.dialogDom_);
this.alert = new d.AlertDialog(this.dialogDom_);
this.confirm = new d.ConfirmDialog(this.dialogDom_);
this.prompt = new d.PromptDialog(this.dialogDom_);
this.shareDialog_ = new ShareDialog(this.dialogDom_);
this.defaultTaskPicker =
new cr.filebrowser.DefaultActionDialog(this.dialogDom_);
};
/**
* One-time initialization of various DOM nodes. Loads the additional DOM
* elements visible to the user. Initialize here elements, which are expensive
* or hidden in the beginning.
*
* @param {function()} callback Completion callback.
* @private
*/
FileManager.prototype.initAdditionalUI_ = function(callback) {
this.initDialogs_();
this.dialogDom_.addEventListener('drop', function(e) {
// Prevent opening an URL by dropping it onto the page.
e.preventDefault();
});
this.dialogDom_.addEventListener('click',
this.onExternalLinkClick_.bind(this));
// Cache nodes we'll be manipulating.
var dom = this.dialogDom_;
this.filenameInput_ = dom.querySelector('#filename-input-box input');
this.taskItems_ = dom.querySelector('#tasks');
this.table_ = dom.querySelector('.detail-table');
this.grid_ = dom.querySelector('.thumbnail-grid');
this.spinner_ = dom.querySelector('#spinner-with-text');
this.showSpinner_(true);
this.searchBreadcrumbs_ = new BreadcrumbsController(
dom.querySelector('#search-breadcrumbs'), this.metadataCache_);
this.searchBreadcrumbs_.addEventListener(
'pathclick', this.onBreadcrumbClick_.bind(this));
this.searchBreadcrumbs_.setHideLast(false);
// Check the option to hide the selecting checkboxes.
this.table_.showCheckboxes = this.showCheckboxes_;
var fullPage = this.dialogType == DialogType.FULL_PAGE;
FileTable.decorate(this.table_, this.metadataCache_, fullPage);
FileGrid.decorate(this.grid_, this.metadataCache_);
this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
this.document_.addEventListener('keyup', this.onKeyUp_.bind(this));
// This capturing event is only used to distinguish focusing using
// keyboard from focusing using mouse.
this.document_.addEventListener('mousedown', function() {
this.suppressFocus_ = true;
}.bind(this), true);
this.renameInput_ = this.document_.createElement('input');
this.renameInput_.className = 'rename';
this.renameInput_.addEventListener(
'keydown', this.onRenameInputKeyDown_.bind(this));
this.renameInput_.addEventListener(
'blur', this.onRenameInputBlur_.bind(this));
this.filenameInput_.addEventListener(
'keydown', this.onFilenameInputKeyDown_.bind(this));
this.filenameInput_.addEventListener(
'focus', this.onFilenameInputFocus_.bind(this));
this.listContainer_ = this.dialogDom_.querySelector('#list-container');
this.listContainer_.addEventListener(
'keydown', this.onListKeyDown_.bind(this));
this.listContainer_.addEventListener(
'keypress', this.onListKeyPress_.bind(this));
this.listContainer_.addEventListener(
'mousemove', this.onListMouseMove_.bind(this));
this.okButton_.addEventListener('click', this.onOk_.bind(this));
this.onCancelBound_ = this.onCancel_.bind(this);
this.cancelButton_.addEventListener('click', this.onCancelBound_);
this.decorateSplitter(
this.dialogDom_.querySelector('div#sidebar-splitter'));
this.decorateSplitter(
this.dialogDom_.querySelector('div#middlebar-splitter'));
this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings');
this.syncButton.addEventListener('activate', this.onDrivePrefClick_.bind(
this, 'cellularDisabled', false /* not inverted */));
this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings');
this.hostedButton.addEventListener('activate', this.onDrivePrefClick_.bind(
this, 'hostedFilesDisabled', true /* inverted */));
this.detailViewButton_ =
this.dialogDom_.querySelector('#detail-view');
this.detailViewButton_.addEventListener('activate',
this.onDetailViewButtonClick_.bind(this));
this.thumbnailViewButton_ =
this.dialogDom_.querySelector('#thumbnail-view');
this.thumbnailViewButton_.addEventListener('activate',
this.onThumbnailViewButtonClick_.bind(this));
cr.ui.ComboButton.decorate(this.taskItems_);
this.taskItems_.addEventListener('select',
this.onTaskItemClicked_.bind(this));
this.dialogDom_.ownerDocument.defaultView.addEventListener(
'resize', this.onResize_.bind(this));
this.filePopup_ = null;
this.searchBoxWrapper_ =
this.dialogDom_.querySelector('.search-box-wrapper');
this.searchBox_ = this.dialogDom_.querySelector('#search-box');
this.searchBox_.addEventListener(
'input', this.onSearchBoxUpdate_.bind(this));
this.searchBox_.addEventListener(
'keydown', this.onSearchBoxKeyDown_.bind(this));
this.searchTextMeasure_ = new TextMeasure(this.searchBox_);
this.searchIcon_ = this.dialogDom_.querySelector('#search-icon');
this.searchIcon_.addEventListener(
'click',
function() { this.searchBox_.focus(); }.bind(this));
this.searchClearButton_ =
this.dialogDom_.querySelector('#search-clear-button');
this.searchClearButton_.addEventListener(
'click',
function() {
this.searchBox_.value = '';
this.onSearchBoxUpdate_();
}.bind(this));
this.lastSearchQuery_ = '';
var autocompleteList = new cr.ui.AutocompleteList();
autocompleteList.id = 'autocomplete-list';
autocompleteList.autoExpands = true;
autocompleteList.requestSuggestions =
this.requestAutocompleteSuggestions_.bind(this);
// function(item) {}.bind(this) does not work here, as it's a constructor.
var self = this;
autocompleteList.itemConstructor = function(item) {
return self.createAutocompleteListItem_(item);
};
// Do nothing when a suggestion is selected.
autocompleteList.handleSelectedSuggestion = function(selectedItem) {};
// Instead, open the suggested item when Enter key is pressed or
// mouse-clicked.
autocompleteList.handleEnterKeydown = function(event) {
this.openAutocompleteSuggestion_();
this.lastAutocompleteQuery_ = '';
this.autocompleteList_.suggestions = [];
}.bind(this);
autocompleteList.addEventListener('mousedown', function(event) {
this.openAutocompleteSuggestion_();
this.lastAutocompleteQuery_ = '';
this.autocompleteList_.suggestions = [];
}.bind(this));
autocompleteList.addEventListener('mouseover', function(event) {
// Change the selection by a mouse over instead of just changing the
// color of moused over element with :hover in CSS. Here's why:
//
// 1) The user selects an item A with up/down keys (item A is highlighted)
// 2) Then the user moves the cursor to another item B
//
// If we just change the color of moused over element (item B), both
// the item A and B are highlighted. This is bad. We should change the
// selection so only the item B is highlighted.
if (event.target.itemInfo)
autocompleteList.selectedItem = event.target.itemInfo;
}.bind(this));
var container = this.document_.querySelector('.dialog-header');
container.appendChild(autocompleteList);
this.autocompleteList_ = autocompleteList;
this.searchBox_.addEventListener('focus', function(event) {
this.autocompleteList_.attachToInput(this.searchBox_);
}.bind(this));
this.searchBox_.addEventListener('blur', function(event) {
this.autocompleteList_.detach();
}.bind(this));
this.defaultActionMenuItem_ =
this.dialogDom_.querySelector('#default-action');
this.openWithCommand_ =
this.dialogDom_.querySelector('#open-with');
this.driveBuyMoreStorageCommand_ =
this.dialogDom_.querySelector('#drive-buy-more-space');
this.defaultActionMenuItem_.addEventListener('activate',
this.dispatchSelectionAction_.bind(this));
this.initFileTypeFilter_();
util.addIsFocusedMethod();
// Populate the static localized strings.
i18nTemplate.process(this.document_, loadTimeData);
// Arrange the file list.
this.table_.normalizeColumns();
this.table_.redraw();
callback();
};
/**
* @private
*/
FileManager.prototype.onBreadcrumbClick_ = function(event) {
this.directoryModel_.changeDirectory(event.path);
};
/**
* Constructs table and grid (heavy operation).
* @private
**/
FileManager.prototype.initFileList_ = function() {
// Always sharing the data model between the detail/thumb views confuses
// them. Instead we maintain this bogus data model, and hook it up to the
// view that is not in use.
this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
var singleSelection =
this.dialogType == DialogType.SELECT_OPEN_FILE ||
this.dialogType == DialogType.SELECT_FOLDER ||
this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
this.dialogType == DialogType.SELECT_SAVEAS_FILE;
var showSpecialSearchRoots =
this.dialogType == DialogType.SELECT_OPEN_FILE ||
this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE ||
this.dialogType == DialogType.FULL_PAGE;
this.fileFilter_ = new FileFilter(
this.metadataCache_,
false /* Don't show dot files by default. */);
this.fileWatcher_ = new FileWatcher(this.metadataCache_);
this.fileWatcher_.addEventListener(
'watcher-metadata-changed',
this.onWatcherMetadataChanged_.bind(this));
this.directoryModel_ = new DirectoryModel(
this.filesystem_.root,
singleSelection,
this.fileFilter_,
this.fileWatcher_,
this.metadataCache_,
this.volumeManager_,
this.isDriveEnabled(),
showSpecialSearchRoots);
this.directoryModel_.start();
this.folderShortcutsModel_ = new FolderShortcutsDataModel();
this.selectionHandler_ = new FileSelectionHandler(this);
this.selectionHandler_.addEventListener('show-preview-panel',
this.onPreviewPanelVisibilityChanged_.bind(this, true));
this.selectionHandler_.addEventListener('hide-preview-panel',
this.onPreviewPanelVisibilityChanged_.bind(this, false));
var dataModel = this.directoryModel_.getFileList();
this.table_.setupCompareFunctions(dataModel);
dataModel.addEventListener('permuted',
this.updateStartupPrefs_.bind(this));
this.directoryModel_.getFileListSelection().addEventListener('change',
this.selectionHandler_.onFileSelectionChanged.bind(
this.selectionHandler_));
this.initList_(this.grid_);
this.initList_(this.table_.list);
var fileListFocusBound = this.onFileListFocus_.bind(this);
var fileListBlurBound = this.onFileListBlur_.bind(this);
this.table_.list.addEventListener('focus', fileListFocusBound);
this.grid_.addEventListener('focus', fileListFocusBound);
this.table_.list.addEventListener('blur', fileListBlurBound);
this.grid_.addEventListener('blur', fileListBlurBound);
var dragStartBound = this.onDragStart_.bind(this);
this.table_.list.addEventListener('dragstart', dragStartBound);
this.grid_.addEventListener('dragstart', dragStartBound);
var dragEndBound = this.onDragEnd_.bind(this);
this.table_.list.addEventListener('dragend', dragEndBound);
this.grid_.addEventListener('dragend', dragEndBound);
// This event is published by DragSelector because drag end event is not
// published at the end of drag selection.
this.table_.list.addEventListener('dragselectionend', dragEndBound);
// TODO(mtomasz, yoshiki): Create sidebar earlier, and here just attach
// the directory model.
this.initSidebar_();
this.table_.addEventListener('column-resize-end',
this.updateStartupPrefs_.bind(this));
// Restore preferences.
this.directoryModel_.sortFileList(
this.viewOptions_.sortField || 'modificationTime',
this.viewOptions_.sortDirection || 'desc');
if (this.viewOptions_.columns) {
var cm = this.table_.columnModel;
for (var i = 0; i < cm.totalSize; i++) {
if (this.viewOptions_.columns[i] > 0)
cm.setWidth(i, this.viewOptions_.columns[i]);
}
}
this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
this.textSearchState_ = {text: '', date: new Date()};
this.closeOnUnmount_ = (this.params_.action == 'auto-open');
if (this.closeOnUnmount_) {
this.volumeManager_.addEventListener('externally-unmounted',
this.onExternallyUnmounted_.bind(this));
}
// Update metadata to change 'Today' and 'Yesterday' dates.
var today = new Date();
today.setHours(0);
today.setMinutes(0);
today.setSeconds(0);
today.setMilliseconds(0);
setTimeout(this.dailyUpdateModificationTime_.bind(this),
today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
};
/**
* @private
*/
FileManager.prototype.initSidebar_ = function() {
this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
DirectoryTree.decorate(this.directoryTree_, this.directoryModel_);
this.navigationList_ = this.dialogDom_.querySelector('#volume-list');
NavigationList.decorate(this.navigationList_, this.directoryModel_);
this.navigationList_.fileManager = this;
this.navigationList_.dataModel =
new NavigationListModel(this.directoryModel_.getRootsList(),
this.folderShortcutsModel_);
this.navigationList_.addEventListener(
'shortcut-target-not-found',
function(e) {
var path = e.path;
var label = e.label;
this.confirm.showWithTitle(
label,
str('SHORTCUT_TARGET_UNAVAILABLE'),
// 'Yes' is clicked.
function() {
this.removeFolderShortcut(path);
}.bind(this));
}.bind(this));
};
/**
* @private
*/
FileManager.prototype.updateMiddleBarVisibility_ = function() {
var currentPath = this.directoryModel_.getCurrentDirPath();
var visible = DirectoryTreeUtil.isEligiblePathForDirectoryTree(currentPath);
this.dialogDom_.
querySelector('.dialog-middlebar-contents').hidden = !visible;
this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible;
this.onResize_();
};
/**
* @private
*/
FileManager.prototype.updateStartupPrefs_ = function() {
var sortStatus = this.directoryModel_.getFileList().sortStatus;
var prefs = {
sortField: sortStatus.field,
sortDirection: sortStatus.direction,
columns: [],
listType: this.listType_
};
var cm = this.table_.columnModel;
for (var i = 0; i < cm.totalSize; i++) {
prefs.columns.push(cm.getWidth(i));
}
if (DialogType.isModal(this.dialogType))
prefs.listType = this.listType;
// Save the global default.
util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs));
// Save the window-specific preference.
if (window.appState) {
window.appState.viewOptions = prefs;
util.saveAppState();
}
};
FileManager.prototype.refocus = function() {
if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
this.filenameInput_.focus();
else
this.currentList_.focus();
};
/**
* File list focus handler. Used to select the top most element on the list
* if nothing was selected.
*
* @private
*/
FileManager.prototype.onFileListFocus_ = function() {
// Do not select default item if focused using mouse.
if (this.suppressFocus_)
return;
var selection = this.getSelection();
if (!selection || selection.totalCount != 0)
return;
this.directoryModel_.selectIndex(0);
};
/**
* File list blur handler.
*
* @private
*/
FileManager.prototype.onFileListBlur_ = function() {
this.suppressFocus_ = false;
};
/**
* Index of selected item in the typeList of the dialog params.
*
* @return {number} 1-based index of selected type or 0 if no type selected.
* @private
*/
FileManager.prototype.getSelectedFilterIndex_ = function() {
var index = Number(this.fileTypeSelector_.selectedIndex);
if (index < 0) // Nothing selected.
return 0;
if (this.params_.includeAllFiles) // Already 1-based.
return index;
return index + 1; // Convert to 1-based;
};
FileManager.prototype.setListType = function(type) {
if (type && type == this.listType_)
return;
this.table_.list.startBatchUpdates();
this.grid_.startBatchUpdates();
// TODO(dzvorygin): style.display and dataModel setting order shouldn't
// cause any UI bugs. Currently, the only right way is first to set display
// style and only then set dataModel.
if (type == FileManager.ListType.DETAIL) {
this.table_.dataModel = this.directoryModel_.getFileList();
this.table_.selectionModel = this.directoryModel_.getFileListSelection();
this.table_.hidden = false;
this.grid_.hidden = true;
this.grid_.selectionModel = this.emptySelectionModel_;
this.grid_.dataModel = this.emptyDataModel_;
this.table_.hidden = false;
/** @type {cr.ui.List} */
this.currentList_ = this.table_.list;
this.detailViewButton_.setAttribute('checked', '');
this.thumbnailViewButton_.removeAttribute('checked');
this.detailViewButton_.setAttribute('disabled', '');
this.thumbnailViewButton_.removeAttribute('disabled');
} else if (type == FileManager.ListType.THUMBNAIL) {
this.grid_.dataModel = this.directoryModel_.getFileList();
this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
this.grid_.hidden = false;
this.table_.hidden = true;
this.table_.selectionModel = this.emptySelectionModel_;
this.table_.dataModel = this.emptyDataModel_;
this.grid_.hidden = false;
/** @type {cr.ui.List} */
this.currentList_ = this.grid_;
this.thumbnailViewButton_.setAttribute('checked', '');
this.detailViewButton_.removeAttribute('checked');
this.thumbnailViewButton_.setAttribute('disabled', '');
this.detailViewButton_.removeAttribute('disabled');
} else {
throw new Error('Unknown list type: ' + type);
}
this.listType_ = type;
this.updateStartupPrefs_();
this.onResize_();
this.table_.list.endBatchUpdates();
this.grid_.endBatchUpdates();
};
/**
* Initialize the file list table or grid.
*
* @param {cr.ui.List} list The list.
* @private
*/
FileManager.prototype.initList_ = function(list) {
// Overriding the default role 'list' to 'listbox' for better accessibility
// on ChromeOS.
list.setAttribute('role', 'listbox');
list.addEventListener('click', this.onDetailClick_.bind(this));
list.id = 'file-list';
};
/**
* @private
*/
FileManager.prototype.onCopyProgress_ = function(event) {
if (event.reason == 'ERROR' &&
event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
event.error.data.toDrive &&
event.error.data.code == FileError.QUOTA_EXCEEDED_ERR) {
this.alert.showHtml(
strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
decodeURIComponent(
event.error.data.sourceFileUrl.split('/').pop()),
FileManager.GOOGLE_DRIVE_BUY_STORAGE));
}
// TODO(benchan): Currently, there is no FileWatcher emulation for
// drive::FileSystem, so we need to manually trigger the directory rescan
// after paste operations complete. Remove this once we emulate file
// watching functionalities in drive::FileSystem.
if (this.isOnDrive()) {
if (event.reason == 'SUCCESS' || event.reason == 'ERROR' ||
event.reason == 'CANCELLED') {
this.directoryModel_.rescanLater();
}
}
};
/**
* Handler of file manager operations. Called when an entry has been
* changed.
* This updates directory model to reflect operation result immediately (not
* waiting for directory update event). Also, preloads thumbnails for the
* images of new entries.
* See also FileCopyManager.EventRouter.
*
* @param {cr.Event} event An event for the entry change.
* @private
*/
FileManager.prototype.onCopyManagerEntryChanged_ = function(event) {
var type = event.type;
var entry = event.entry;
this.directoryModel_.onEntryChanged(type, entry);
if (type == util.EntryChangedType.CREATE && FileType.isImage(entry)) {
// Preload a thumbnail if the new copied entry an image.
var metadata = entry.getMetadata(function(metadata) {
var url = entry.toURL();
var thumbnailLoader_ = new ThumbnailLoader(
url,
ThumbnailLoader.LoaderType.CANVAS,
metadata,
undefined, // Media type.
FileType.isOnDrive(url) ?
ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
10); // Very low priority.
thumbnailLoader_.loadDetachedImage(function(success) {});
});
}
};
/**
* Fills the file type list or hides it.
* @private
*/
FileManager.prototype.initFileTypeFilter_ = function() {
if (this.params_.includeAllFiles) {
var option = this.document_.createElement('option');
option.innerText = str('ALL_FILES_FILTER');
this.fileTypeSelector_.appendChild(option);
option.value = 0;
}
for (var i = 0; i < this.fileTypes_.length; i++) {
var fileType = this.fileTypes_[i];
var option = this.document_.createElement('option');
var description = fileType.description;
if (!description) {
// See if all the extensions in the group have the same description.
for (var j = 0; j != fileType.extensions.length; j++) {
var currentDescription =
FileType.getTypeString('.' + fileType.extensions[j]);
if (!description) // Set the first time.
description = currentDescription;
else if (description != currentDescription) {
// No single description, fall through to the extension list.
description = null;
break;
}
}
if (!description)
// Convert ['jpg', 'png'] to '*.jpg, *.png'.
description = fileType.extensions.map(function(s) {
return '*.' + s;
}).join(', ');
}
option.innerText = description;
option.value = i + 1;
if (fileType.selected)
option.selected = true;
this.fileTypeSelector_.appendChild(option);
}
var options = this.fileTypeSelector_.querySelectorAll('option');
if (options.length < 2) {
// There is in fact no choice, hide the selector.
this.fileTypeSelector_.hidden = true;
return;
}
this.fileTypeSelector_.addEventListener('change',
this.updateFileTypeFilter_.bind(this));
};
/**
* Filters file according to the selected file type.
* @private
*/
FileManager.prototype.updateFileTypeFilter_ = function() {
this.fileFilter_.removeFilter('fileType');
var selectedIndex = this.getSelectedFilterIndex_();
if (selectedIndex > 0) { // Specific filter selected.
var regexp = new RegExp('.*(' +
this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
var filter = function(entry) {
return entry.isDirectory || regexp.test(entry.name);
};
this.fileFilter_.addFilter('fileType', filter);
}
};
/**
* Resize details and thumb views to fit the new window size.
* @private
*/
FileManager.prototype.onResize_ = function() {
if (this.listType_ == FileManager.ListType.THUMBNAIL)
this.grid_.relayout();
else
this.table_.relayout();
// May not be available during initialization.
if (this.directoryTree_)
this.directoryTree_.relayout();
// TODO(mtomasz, yoshiki): Initialize navigation list earlier, before
// file system is available.
if (this.navigationList_)
this.navigationList_.redraw();
// Hide the search box if there is not enough space.
this.searchBoxWrapper_.classList.toggle(
'too-short',
this.searchBoxWrapper_.clientWidth < 100);
this.searchBreadcrumbs_.truncate();
};
/**
* Handles local metadata changes in the currect directory.
* @param {Event} event Change event.
* @private
*/
FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
this.updateMetadataInUI_(event.metadataType, event.urls, event.properties);
};
/**
* Resize details and thumb views to fit the new window size.
* @private
*/
FileManager.prototype.onPreviewPanelVisibilityChanged_ = function(visible) {
var panelHeight = visible ? this.getPreviewPanelHeight_() : 0;
this.grid_.setBottomMarginForPanel(panelHeight);
this.table_.setBottomMarginForPanel(panelHeight);
this.directoryTree_.setBottomMarginForPanel(panelHeight);
};
/**
* Invoked when the drag is started on the list or the grid.
* @private
*/
FileManager.prototype.onDragStart_ = function() {
this.selectionHandler_.setPreviewPanelMustBeHidden(true);
};
/**
* Invoked when the drag is ended on the list or the grid.
* @private
*/
FileManager.prototype.onDragEnd_ = function() {
this.selectionHandler_.setPreviewPanelMustBeHidden(false);
};
/**
* Gets height of the preview panel, using cached value if available. This
* returns the value even when the preview panel is hidden.
*
* @return {number} Height of the preview panel. If failure, returns 0.
*/
FileManager.prototype.getPreviewPanelHeight_ = function() {
if (!this.cachedPreviewPanelHeight_) {
var previewPanel = this.dialogDom_.querySelector('.preview-panel');
this.cachedPreviewPanelHeight_ = previewPanel.clientHeight;
}
return this.cachedPreviewPanelHeight_;
};
/**
* Restores current directory and may be a selected item after page load (or
* reload) or popping a state (after click on back/forward). If location.hash
* is present it means that the user has navigated somewhere and that place
* will be restored. defaultPath primarily is used with save/open dialogs.
* Default path may also contain a file name. Freshly opened file manager
* window has neither.
*
* @param {boolean} pageLoading True if the page is loading,
* false if popping state.
* @private
*/
FileManager.prototype.setupCurrentDirectory_ = function(pageLoading) {
var path = location.hash ? // Location hash has the highest priority.
decodeURIComponent(location.hash.substr(1)) :
this.defaultPath;
if (!pageLoading && path == this.directoryModel_.getCurrentDirPath())
return;
if (!path) {
path = PathUtil.DEFAULT_DIRECTORY;
} else if (path.indexOf('/') == -1) {
// Path is a file name.
path = PathUtil.DEFAULT_DIRECTORY + '/' + path;
}
// In the FULL_PAGE mode if the hash path points to a file we might have
// to invoke a task after selecting it.
// If the file path is in params_ we only want to select the file.
var invokeHandlers = pageLoading && (this.params_.action != 'select') &&
this.dialogType == DialogType.FULL_PAGE;
if (PathUtil.getRootType(path) === RootType.DRIVE) {
if (!this.isDriveEnabled()) {
var leafName = path.substr(path.indexOf('/') + 1);
path = this.directoryModel_.getDefaultDirectory() + '/' + leafName;
this.finishSetupCurrentDirectory_(path, invokeHandlers);
return;
}
if (this.volumeManager_.isMounted(RootDirectory.DRIVE)) {
this.finishSetupCurrentDirectory_(path, invokeHandlers);
return;
}
var tracker = this.directoryModel_.createDirectoryChangeTracker();
// Expected finish of setupPath to Drive.
tracker.exceptInitialChange = true;
tracker.start();
// Waits until the Drive is mounted.
this.volumeManager_.mountDrive(function() {
tracker.stop();
if (!tracker.hasChanged)
this.finishSetupCurrentDirectory_(path, invokeHandlers);
}.bind(this), function(error) {
tracker.stop();
});
} else {
this.finishSetupCurrentDirectory_(path, invokeHandlers);
}
};
/**
* @param {string} path Path to setup.
* @param {boolean} invokeHandlers If thrue and |path| points to a file
* then default handler is triggered.
*
* @private
*/
FileManager.prototype.finishSetupCurrentDirectory_ = function(
path, invokeHandlers) {
if (invokeHandlers) {
var onResolve = function(baseName, leafName, exists) {
var urls = null;
var action = null;
if (!exists || leafName == '') {
// Non-existent file or a directory.
if (this.params_.gallery) {
// Reloading while the Gallery is open with empty or multiple
// selection. Open the Gallery when the directory is scanned.
urls = [];
action = 'gallery';
}
} else {
// There are 3 ways we can get here:
// 1. Invoked from file_manager_util::ViewFile. This can only
// happen for 'gallery' and 'mount-archive' actions.
// 2. Reloading a Gallery page. Must be an image or a video file.
// 3. A user manually entered a URL pointing to a file.
// We call the appropriate methods of FileTasks directly as we do
// not need any of the preparations that |execute| method does.
if (FileType.isImageOrVideo(path)) {
urls = [util.makeFilesystemUrl(path)];
action = 'gallery';
}
if (FileType.getMediaType(path) == 'archive') {
urls = [util.makeFilesystemUrl(path)];
action = 'archives';
}
}
if (urls) {
var listener = function() {
this.directoryModel_.removeEventListener(
'scan-completed', listener);
var tasks = new FileTasks(this, this.params_);
if (action == 'gallery') {
tasks.openGallery(urls);
} else if (action == 'archives') {
tasks.mountArchives(urls);
}
}.bind(this);
this.directoryModel_.addEventListener('scan-completed', listener);
}
}.bind(this);
this.directoryModel_.setupPath(path, onResolve);
return;
}
if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
this.directoryModel_.setupPath(path, function(basePath, leafName) {
this.filenameInput_.value = leafName;
this.selectDefaultPathInFilenameInput_();
}.bind(this));
return;
}
this.directoryModel_.setupPath(path);
};
/**
* Tweak the UI to become a particular kind of dialog, as determined by the
* dialog type parameter passed to the constructor.
*
* @private
*/
FileManager.prototype.initDialogType_ = function() {
var defaultTitle;
var okLabel = str('OPEN_LABEL');
switch (this.dialogType) {
case DialogType.SELECT_FOLDER:
defaultTitle = str('SELECT_FOLDER_TITLE');
break;
case DialogType.SELECT_UPLOAD_FOLDER:
defaultTitle = str('SELECT_UPLOAD_FOLDER_TITLE');
okLabel = str('UPLOAD_LABEL');
break;
case DialogType.SELECT_OPEN_FILE:
defaultTitle = str('SELECT_OPEN_FILE_TITLE');
break;
case DialogType.SELECT_OPEN_MULTI_FILE:
defaultTitle = str('SELECT_OPEN_MULTI_FILE_TITLE');
break;
case DialogType.SELECT_SAVEAS_FILE:
defaultTitle = str('SELECT_SAVEAS_FILE_TITLE');
okLabel = str('SAVE_LABEL');
break;
case DialogType.FULL_PAGE:
break;
default:
throw new Error('Unknown dialog type: ' + this.dialogType);
}
this.okButton_.textContent = okLabel;
this.dialogDom_.setAttribute('type', this.dialogType);
};
/**
* Unmounts device.
* @param {string} path Path to a volume to unmount.
*/
FileManager.prototype.unmountVolume = function(path) {
var onError = function(error) {
this.alert.showHtml('', str('UNMOUNT_FAILED'));
};
this.volumeManager_.unmount(path, function() {}, onError.bind(this));
};
/**
* @private
*/
FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
var entries = this.directoryModel_.getFileList().slice();
var directoryEntry = this.directoryModel_.getCurrentDirEntry();
// We don't pass callback here. When new metadata arrives, we have an
// observer registered to update the UI.
// TODO(dgozman): refresh content metadata only when modificationTime
// changed.
var isFakeEntry = typeof directoryEntry.toURL !== 'function';
var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
this.metadataCache_.clearRecursively(directoryEntry, '*');
this.metadataCache_.get(getEntries, 'filesystem', null);
if (this.isOnDrive())
this.metadataCache_.get(getEntries, 'drive', null);
var visibleItems = this.currentList_.items;
var visibleEntries = [];
for (var i = 0; i < visibleItems.length; i++) {
var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
var entry = this.directoryModel_.getFileList().item(index);
// The following check is a workaround for the bug in list: sometimes item
// does not have listIndex, and therefore is not found in the list.
if (entry) visibleEntries.push(entry);
}
this.metadataCache_.get(visibleEntries, 'thumbnail', null);
};
/**
* @private
*/
FileManager.prototype.dailyUpdateModificationTime_ = function() {
var fileList = this.directoryModel_.getFileList();
var urls = [];
for (var i = 0; i < fileList.length; i++) {
urls.push(fileList.item(i).toURL());
}
this.metadataCache_.get(
fileList.slice(), 'filesystem',
this.updateMetadataInUI_.bind(this, 'filesystem', urls));
setTimeout(this.dailyUpdateModificationTime_.bind(this),
MILLISECONDS_IN_DAY);
};
/**
* @param {string} type Type of metadata changed.
* @param {Array.<string>} urls Array of urls.
* @param {Object.<string, Object>} props Map from entry URLs to metadata
* props.
* @private
*/
FileManager.prototype.updateMetadataInUI_ = function(
type, urls, properties) {
var propertyByUrl = urls.reduce(function(map, url, index) {
map[url] = properties[index];
return map;
}, {});
if (this.listType_ == FileManager.ListType.DETAIL)
this.table_.updateListItemsMetadata(type, propertyByUrl);
else
this.grid_.updateListItemsMetadata(type, propertyByUrl);
// TODO: update bottom panel thumbnails.
};
/**
* Restore the item which is being renamed while refreshing the file list. Do
* nothing if no item is being renamed or such an item disappeared.
*
* While refreshing file list it gets repopulated with new file entries.
* There is not a big difference whether DOM items stay the same or not.
* Except for the item that the user is renaming.
*
* @private
*/
FileManager.prototype.restoreItemBeingRenamed_ = function() {
if (!this.isRenamingInProgress())
return;
var dm = this.directoryModel_;
var leadIndex = dm.getFileListSelection().leadIndex;
if (leadIndex < 0)
return;
var leadEntry = dm.getFileList().item(leadIndex);
if (this.renameInput_.currentEntry.fullPath != leadEntry.fullPath)
return;
var leadListItem = this.findListItemForNode_(this.renameInput_);
if (this.currentList_ == this.table_.list) {
this.table_.updateFileMetadata(leadListItem, leadEntry);
}
this.currentList_.restoreLeadItem(leadListItem);
};
/**
* @return {boolean} True if the current directory content is from Google
* Drive.
*/
FileManager.prototype.isOnDrive = function() {
var rootType = this.directoryModel_.getCurrentRootType();
return rootType === RootType.DRIVE ||
rootType === RootType.DRIVE_SHARED_WITH_ME ||
rootType === RootType.DRIVE_RECENT ||
rootType === RootType.DRIVE_OFFLINE;
};
/**
* @return {boolean} True if the ctrl key is pressed now.
*/
FileManager.prototype.isCtrlKeyPressed = function() {
return this.ctrlKeyPressed_;
};
/**
* Overrides default handling for clicks on hyperlinks.
* In a packaged apps links with targer='_blank' open in a new tab by
* default, other links do not open at all.
*
* @param {Event} event Click event.
* @private
*/
FileManager.prototype.onExternalLinkClick_ = function(event) {
if (event.target.tagName != 'A' || !event.target.href)
return;
if (this.dialogType != DialogType.FULL_PAGE)
this.onCancel_();
};
/**
* Task combobox handler.
*
* @param {Object} event Event containing task which was clicked.
* @private
*/
FileManager.prototype.onTaskItemClicked_ = function(event) {
var selection = this.getSelection();
if (!selection.tasks) return;
if (event.item.task) {
// Task field doesn't exist on change-default dropdown item.
selection.tasks.execute(event.item.task.taskId);
} else {
var extensions = [];
for (var i = 0; i < selection.urls.length; i++) {
var match = /\.(\w+)$/g.exec(selection.urls[i]);
if (match) {
var ext = match[1].toUpperCase();
if (extensions.indexOf(ext) == -1) {
extensions.push(ext);
}
}
}
var format = '';
if (extensions.length == 1) {
format = extensions[0];
}
// Change default was clicked. We should open "change default" dialog.
selection.tasks.showTaskPicker(this.defaultTaskPicker,
loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
strf('CHANGE_DEFAULT_CAPTION', format),
this.onDefaultTaskDone_.bind(this));
}
};
/**
* Sets the given task as default, when this task is applicable.
*
* @param {Object} task Task to set as default.
* @private
*/
FileManager.prototype.onDefaultTaskDone_ = function(task) {
// TODO(dgozman): move this method closer to tasks.
var selection = this.getSelection();
chrome.fileBrowserPrivate.setDefaultTask(task.taskId,
selection.urls, selection.mimeTypes);
selection.tasks = new FileTasks(this);
selection.tasks.init(selection.urls, selection.mimeTypes);
selection.tasks.display(this.taskItems_);
this.refreshCurrentDirectoryMetadata_();
this.selectionHandler_.onFileSelectionChanged();
};
/**
* @private
*/
FileManager.prototype.onPreferencesChanged_ = function() {
var self = this;
this.getPreferences_(function(prefs) {
self.initDateTimeFormatters_();
self.refreshCurrentDirectoryMetadata_();
self.directoryModel_.setDriveEnabled(self.isDriveEnabled());
if (prefs.cellularDisabled)
self.syncButton.setAttribute('checked', '');
else
self.syncButton.removeAttribute('checked');
if (self.hostedButton.hasAttribute('checked') !=
prefs.hostedFilesDisabled && self.isOnDrive()) {
self.directoryModel_.rescan();
}
if (!prefs.hostedFilesDisabled)
self.hostedButton.setAttribute('checked', '');
else
self.hostedButton.removeAttribute('checked');
},
true /* refresh */);
};
FileManager.prototype.onDriveConnectionChanged_ = function() {
var connection = this.volumeManager_.getDriveConnectionState();
this.updateCommands();
if (this.dialogContainer_)
this.dialogContainer_.setAttribute('connection', connection.type);
if (this.shareDialog_.isShowing()) {
this.shareDialog_.hide();
this.error.show(str('SHARE_ERROR'));
}
};
/**
* Get the metered status of Drive connection.
*
* @return {boolean} Returns true if drive should limit the traffic because
* the connection is metered and the 'disable-sync-on-metered' setting is
* enabled. Otherwise, returns false.
*/
FileManager.prototype.isDriveOnMeteredConnection = function() {
var connection = this.volumeManager_.getDriveConnectionState();
return connection.type == VolumeManager.DriveConnectionType.METERED;
};
/**
* Get the online/offline status of drive.
*
* @return {boolean} Returns true if the connection is offline. Otherwise,
* returns false.
*/
FileManager.prototype.isDriveOffline = function() {
var connection = this.volumeManager_.getDriveConnectionState();
return connection.type == VolumeManager.DriveConnectionType.OFFLINE;
};
FileManager.prototype.isDriveEnabled = function() {
// Auto resolving to local path does not work for folders (e.g., dialog for
// loading unpacked extensions).
var noLocalPathResolution =
this.params_.type == DialogType.SELECT_FOLDER ||
this.params_.type == DialogType.SELECT_UPLOAD_FOLDER;
if (noLocalPathResolution && this.params_.shouldReturnLocalPath)
return false;
return this.preferences_.driveEnabled;
};
FileManager.prototype.isOnReadonlyDirectory = function() {
return this.directoryModel_.isReadOnly();
};
/**
* @param {Event} Unmount event.
* @private
*/
FileManager.prototype.onExternallyUnmounted_ = function(event) {
if (event.mountPath == this.directoryModel_.getCurrentRootPath()) {
if (this.closeOnUnmount_) {
// If the file manager opened automatically when a usb drive inserted,
// user have never changed current volume (that implies the current
// directory is still on the device) then close this window.
window.close();
}
}
};
/**
* Show a modal-like file viewer/editor on top of the File Manager UI.
*
* @param {HTMLElement} popup Popup element.
* @param {function} closeCallback Function to call after the popup is closed.
*
* @private
*/
FileManager.prototype.openFilePopup_ = function(popup, closeCallback) {
this.closeFilePopup_();
this.filePopup_ = popup;
this.filePopupCloseCallback_ = closeCallback;
this.dialogDom_.appendChild(this.filePopup_);
this.filePopup_.focus();
this.document_.body.setAttribute('overlay-visible', '');
this.document_.querySelector('#iframe-drag-area').hidden = false;
};
/**
* @private
*/
FileManager.prototype.closeFilePopup_ = function() {
if (this.filePopup_) {
this.document_.body.removeAttribute('overlay-visible');
this.document_.querySelector('#iframe-drag-area').hidden = true;
// The window resize would not be processed properly while the relevant
// divs had 'display:none', force resize after the layout fired.
setTimeout(this.onResize_.bind(this), 0);
if (this.filePopup_.contentWindow &&
this.filePopup_.contentWindow.unload) {
this.filePopup_.contentWindow.unload();
}
if (this.filePopupCloseCallback_) {
this.filePopupCloseCallback_();
this.filePopupCloseCallback_ = null;
}
// These operations have to be in the end, otherwise v8 crashes on an
// assert. See: crbug.com/224174.
this.dialogDom_.removeChild(this.filePopup_);
this.filePopup_ = null;
}
};
FileManager.prototype.getAllUrlsInCurrentDirectory = function() {
var urls = [];
var fileList = this.directoryModel_.getFileList();
for (var i = 0; i != fileList.length; i++) {
urls.push(fileList.item(i).toURL());
}
return urls;
};
FileManager.prototype.isRenamingInProgress = function() {
return !!this.renameInput_.currentEntry;
};
/**
* @private
*/
FileManager.prototype.focusCurrentList_ = function() {
if (this.listType_ == FileManager.ListType.DETAIL)
this.table_.focus();
else // this.listType_ == FileManager.ListType.THUMBNAIL)
this.grid_.focus();
};
/**
* Return full path of the current directory or null.
* @return {?string} The full path of the current directory.
*/
FileManager.prototype.getCurrentDirectory = function() {
return this.directoryModel_ &&
this.directoryModel_.getCurrentDirPath();
};
/**
* Return URL of the current directory or null.
* @return {string} URL representing the current directory.
*/
FileManager.prototype.getCurrentDirectoryURL = function() {
return this.directoryModel_ &&
this.directoryModel_.getCurrentDirectoryURL();
};
/**
* Return DirectoryEntry of the current directory or null.
* @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
* null if the directory model is not ready or the current directory is
* not set.
*/
FileManager.prototype.getCurrentDirectoryEntry = function() {
return this.directoryModel_ &&
this.directoryModel_.getCurrentDirEntry();
};
/**
* Deletes the selected file and directories recursively.
*/
FileManager.prototype.deleteSelection = function() {
// TODO(mtomasz): Remove this temporary dialog. crbug.com/167364
var entries = this.getSelection().entries;
var message = entries.length == 1 ?
strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) :
strf('GALLERY_CONFIRM_DELETE_SOME', entries.length);
this.confirm.show(message, function() {
this.copyManager_.deleteEntries(entries);
}.bind(this));
};
/**
* Shows the share dialog for the selected file or directory.
*/
FileManager.prototype.shareSelection = function() {
var entries = this.getSelection().entries;
if (entries.length != 1) {
console.warn('Unable to share multiple items at once.');
return;
}
this.shareDialog_.show(entries[0], function() {
this.error.show(str('SHARE_ERROR'));
}.bind(this));
};
/**
* Folder shared feature is under development and hidden behind flag. This
* method returns if the feature is explicitly enabled by the flag or not.
* TODO(yoshiki): Remove this after launching folder feature feature.
*
* @return {boolena} True if the flag is enabled.
*/
FileManager.prototype.isFolderShortcutsEnabled = function() {
// TODO(yoshiki): Remove this method in M31.
return true;
};
/**
* Creates a folder shortcut.
* @param {string} path A shortcut which refers to |path| to be created.
*/
FileManager.prototype.createFolderShortcut = function(path) {
// Duplicate entry.
if (this.folderShortcutExists(path))
return;
this.folderShortcutsModel_.add(path);
};
/**
* Checkes if the shortcut which refers to the given folder exists or not.
* @param {string} path Path of the folder to be checked.
*/
FileManager.prototype.folderShortcutExists = function(path) {
return this.folderShortcutsModel_.exists(path);
};
/**
* Removes the folder shortcut.
* @param {string} path The shortcut which refers to |path| is to be removed.
*/
FileManager.prototype.removeFolderShortcut = function(path) {
this.folderShortcutsModel_.remove(path);
};
/**
* Blinks the selection. Used to give feedback when copying or cutting the
* selection.
*/
FileManager.prototype.blinkSelection = function() {
var selection = this.getSelection();
if (!selection || selection.totalCount == 0)
return;
for (var i = 0; i < selection.entries.length; i++) {
var selectedIndex = selection.indexes[i];
var listItem = this.currentList_.getListItemByIndex(selectedIndex);
if (listItem)
this.blinkListItem_(listItem);
}
};
/**
* @param {Element} listItem List item element.
* @private
*/
FileManager.prototype.blinkListItem_ = function(listItem) {
listItem.classList.add('blink');
setTimeout(function() {
listItem.classList.remove('blink');
}, 100);
};
/**
* @private
*/
FileManager.prototype.selectDefaultPathInFilenameInput_ = function() {
var input = this.filenameInput_;
input.focus();
var selectionEnd = input.value.lastIndexOf('.');
if (selectionEnd == -1) {
input.select();
} else {
input.selectionStart = 0;
input.selectionEnd = selectionEnd;
}
// Clear, so we never do this again.
this.defaultPath = '';
};
/**
* Handles mouse click or tap.
*
* @param {Event} event The click event.
* @private
*/
FileManager.prototype.onDetailClick_ = function(event) {
if (this.isRenamingInProgress()) {
// Don't pay attention to clicks during a rename.
return;
}
var listItem = this.findListItemForEvent_(event);
var selection = this.getSelection();
if (!listItem || !listItem.selected || selection.totalCount != 1) {
return;
}
// React on double click, but only if both clicks hit the same item.
// TODO(mtomasz): Simplify it, and use a double click handler if possible.
var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
this.lastClickedItem_ = listItem;
if (event.detail != clickNumber)
return;
var entry = selection.entries[0];
if (entry.isDirectory) {
this.onDirectoryAction(entry);
} else {
this.dispatchSelectionAction_();
}
};
/**
* @private
*/
FileManager.prototype.dispatchSelectionAction_ = function() {
if (this.dialogType == DialogType.FULL_PAGE) {
var tasks = this.getSelection().tasks;
if (tasks) tasks.executeDefault();
return true;
}
if (!this.okButton_.disabled) {
this.onOk_();
return true;
}
return false;
};
/**
* Executes directory action (i.e. changes directory).
*
* @param {DirectoryEntry} entry Directory entry to which directory should be
* changed.
*/
FileManager.prototype.onDirectoryAction = function(entry) {
var mountError = this.volumeManager_.getMountError(
PathUtil.getRootPath(entry.fullPath));
if (mountError == VolumeManager.Error.UNKNOWN_FILESYSTEM) {
return this.butterBar_.show(ButterBar.Mode.ERROR,
str('UNKNOWN_FILESYSTEM_WARNING'));
} else if (mountError == VolumeManager.Error.UNSUPPORTED_FILESYSTEM) {
return this.butterBar_.show(ButterBar.Mode.ERROR,
str('UNSUPPORTED_FILESYSTEM_WARNING'));
}
return this.directoryModel_.changeDirectory(entry.fullPath);
};
/**
* Update the window title.
* @private
*/
FileManager.prototype.updateTitle_ = function() {
if (this.dialogType != DialogType.FULL_PAGE)
return;
var path = this.getCurrentDirectory();
var rootPath = PathUtil.getRootPath(path);
this.document_.title = PathUtil.getRootLabel(rootPath) +
path.substring(rootPath.length);
};
/**
* Updates search box value when directory gets changed.
* @private
*/
FileManager.prototype.updateSearchBoxOnDirChange_ = function() {
if (!this.searchBox_.disabled) {
this.searchBox_.value = '';
this.updateSearchBoxStyles_();
}
};
/**
* Update the gear menu.
* @private
*/
FileManager.prototype.updateGearMenu_ = function() {
var hideItemsForDrive = !this.isOnDrive();
this.syncButton.hidden = hideItemsForDrive;
this.hostedButton.hidden = hideItemsForDrive;
this.document_.getElementById('drive-separator').hidden =
hideItemsForDrive;
// If volume has changed, then fetch remaining space data.
if (this.previousRootUrl_ != this.directoryModel_.getCurrentMountPointUrl())
this.refreshRemainingSpace_(true); // Show loading caption.
this.previousRootUrl_ = this.directoryModel_.getCurrentMountPointUrl();
};
/**
* Refreshes space info of the current volume.
* @param {boolean} showLoadingCaption Whether show loading caption or not.
* @private
*/
FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
var volumeSpaceInfoLabel =
this.dialogDom_.querySelector('#volume-space-info-label');
var volumeSpaceInnerBar =
this.dialogDom_.querySelector('#volume-space-info-bar');
var volumeSpaceOuterBar =
this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
volumeSpaceInnerBar.setAttribute('pending', '');
if (showLoadingCaption) {
volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
volumeSpaceInnerBar.style.width = '100%';
}
var currentMountPointUrl = this.directoryModel_.getCurrentMountPointUrl();
chrome.fileBrowserPrivate.getSizeStats(
currentMountPointUrl, function(result) {
if (this.directoryModel_.getCurrentMountPointUrl() !=
currentMountPointUrl)
return;
updateSpaceInfo(result,
volumeSpaceInnerBar,
volumeSpaceInfoLabel,
volumeSpaceOuterBar);
}.bind(this));
};
/**
* Update the UI when the current directory changes.
*
* @param {cr.Event} event The directory-changed event.
* @private
*/
FileManager.prototype.onDirectoryChanged_ = function(event) {
this.selectionHandler_.onFileSelectionChanged();
this.updateSearchBoxOnDirChange_();
util.updateAppState(this.getCurrentDirectory());
if (this.closeOnUnmount_ && !event.initial &&
PathUtil.getRootPath(event.previousDirEntry.fullPath) !=
PathUtil.getRootPath(event.newDirEntry.fullPath)) {
this.closeOnUnmount_ = false;
}
this.updateCommands();
this.updateUnformattedDriveStatus_();
this.updateTitle_();
this.updateGearMenu_();
};
/**
* Updates commands' states by emiting canExecute events. Should be used
* only if there is need to reevaluate states without an user action, eg.
* external events.
*/
FileManager.prototype.updateCommands = function() {
var commands = this.dialogDom_.querySelectorAll('command');
for (var i = 0; i < commands.length; i++) {
// Commands may not have been decorated yet.
if (commands[i].canExecuteChange)
commands[i].canExecuteChange();
}
};
// TODO(haruki): Rename this method. "Drive" here does not refer
// "Google Drive".
FileManager.prototype.updateUnformattedDriveStatus_ = function() {
var volumeInfo = this.volumeManager_.getVolumeInfo_(
PathUtil.getRootPath(this.directoryModel_.getCurrentRootPath()));
if (volumeInfo.error) {
this.dialogDom_.setAttribute('unformatted', '');
var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
if (volumeInfo.error == VolumeManager.Error.UNSUPPORTED_FILESYSTEM) {
errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
} else {
errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
}
// Update 'canExecute' for format command so the format button's disabled
// property is properly set.
this.updateCommands();
} else {
this.dialogDom_.removeAttribute('unformatted');
}
};
FileManager.prototype.findListItemForEvent_ = function(event) {
return this.findListItemForNode_(event.touchedElement || event.srcElement);
};
FileManager.prototype.findListItemForNode_ = function(node) {
var item = this.currentList_.getListItemAncestor(node);
// TODO(serya): list should check that.
return item && this.currentList_.isItem(item) ? item : null;
};
/**
* Unload handler for the page. May be called manually for the file picker
* dialog, because it closes by calling extension API functions that do not
* return.
*
* @private
*/
FileManager.prototype.onUnload_ = function() {
if (this.directoryModel_)
this.directoryModel_.dispose();
if (this.filePopup_ &&
this.filePopup_.contentWindow &&
this.filePopup_.contentWindow.unload)
this.filePopup_.contentWindow.unload(true /* exiting */);
if (this.butterBar_)
this.butterBar_.dispose();
if (this.copyManager_) {
if (this.onCopyProgressBound_) {
this.copyManager_.removeEventListener(
'copy-progress', this.onCopyProgressBound_);
}
if (this.onCopyManagerEntryChangedBound_) {
this.copyManager_.removeEventListener(
'entry-changed', this.onCopyManagerEntryChangedBound_);
}
}
};
FileManager.prototype.initiateRename = function() {
var item = this.currentList_.ensureLeadItemExists();
if (!item)
return;
var label = item.querySelector('.filename-label');
var input = this.renameInput_;
input.value = label.textContent;
label.parentNode.setAttribute('renaming', '');
label.parentNode.appendChild(input);
input.focus();
var selectionEnd = input.value.lastIndexOf('.');
if (selectionEnd == -1) {
input.select();
} else {
input.selectionStart = 0;
input.selectionEnd = selectionEnd;
}
// This has to be set late in the process so we don't handle spurious
// blur events.
input.currentEntry = this.currentList_.dataModel.item(item.listIndex);
};
/**
* @type {Event} Key event.
* @private
*/
FileManager.prototype.onRenameInputKeyDown_ = function(event) {
if (!this.isRenamingInProgress())
return;
// Do not move selection or lead item in list during rename.
if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
event.stopPropagation();
}
switch (util.getKeyModifiers(event) + event.keyCode) {
case '27': // Escape
this.cancelRename_();
event.preventDefault();
break;
case '13': // Enter
this.commitRename_();
event.preventDefault();
break;
}
};
/**
* @type {Event} Blur event.
* @private
*/
FileManager.prototype.onRenameInputBlur_ = function(event) {
if (this.isRenamingInProgress() && !this.renameInput_.validation_)
this.commitRename_();
};
/**
* @private
*/
FileManager.prototype.commitRename_ = function() {
var input = this.renameInput_;
var entry = input.currentEntry;
var newName = input.value;
if (newName == entry.name) {
this.cancelRename_();
return;
}
var nameNode = this.findListItemForNode_(this.renameInput_).
querySelector('.filename-label');
input.validation_ = true;
var validationDone = function(valid) {
input.validation_ = false;
// Alert dialog restores focus unless the item removed from DOM.
if (this.document_.activeElement != input)
this.cancelRename_();
if (!valid)
return;
// Validation succeeded. Do renaming.
this.cancelRename_();
// Optimistically apply new name immediately to avoid flickering in
// case of success.
nameNode.textContent = newName;
this.directoryModel_.doesExist(entry, newName, function(exists, isFile) {
if (!exists) {
var onError = function(err) {
this.alert.show(strf('ERROR_RENAMING', entry.name,
util.getFileErrorString(err.code)));
}.bind(this);
this.directoryModel_.renameEntry(entry, newName, onError.bind(this));
} else {
nameNode.textContent = entry.name;
var message = isFile ? 'FILE_ALREADY_EXISTS' :
'DIRECTORY_ALREADY_EXISTS';
this.alert.show(strf(message, newName));
}
}.bind(this));
};
// TODO(haruki): this.getCurrentDirectoryURL() might not return the actual
// parent if the directory content is a search result. Fix it to do proper
// validation.
this.validateFileName_(this.getCurrentDirectoryURL(),
newName,
validationDone.bind(this));
};
/**
* @private
*/
FileManager.prototype.cancelRename_ = function() {
this.renameInput_.currentEntry = null;
var parent = this.renameInput_.parentNode;
if (parent) {
parent.removeAttribute('renaming');
parent.removeChild(this.renameInput_);
}
};
/**
* @param {Event} Key event.
* @private
*/
FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
var enabled = this.selectionHandler_.updateOkButton();
if (enabled &&
(util.getKeyModifiers(event) + event.keyCode) == '13' /* Enter */)
this.onOk_();
};
/**
* @param {Event} Focus event.
* @private
*/
FileManager.prototype.onFilenameInputFocus_ = function(event) {
var input = this.filenameInput_;
// On focus we want to select everything but the extension, but
// Chrome will select-all after the focus event completes. We
// schedule a timeout to alter the focus after that happens.
setTimeout(function() {
var selectionEnd = input.value.lastIndexOf('.');
if (selectionEnd == -1) {
input.select();
} else {
input.selectionStart = 0;
input.selectionEnd = selectionEnd;
}
}, 0);
};
/**
* @private
*/
FileManager.prototype.onScanStarted_ = function() {
if (this.scanInProgress_ && !this.scanUpdatedAtLeastOnceOrCompleted_) {
this.table_.list.endBatchUpdates();
this.grid_.endBatchUpdates();
}
this.updateCommands();
this.table_.list.startBatchUpdates();
this.grid_.startBatchUpdates();
this.scanInProgress_ = true;
this.scanUpdatedAtLeastOnceOrCompleted_ = false;
if (this.scanCompletedTimer_) {
clearTimeout(this.scanCompletedTimer_);
this.scanCompletedTimer_ = null;
}
if (this.scanUpdatedTimer_) {
clearTimeout(this.scanUpdatedTimer_);
this.scanUpdatedTimer_ = null;
}
if (this.spinner_.hidden) {
this.cancelSpinnerTimeout_();
this.showSpinnerTimeout_ =
setTimeout(this.showSpinner_.bind(this, true), 500);
}
};
/**
* @private
*/
FileManager.prototype.onScanCompleted_ = function() {
if (!this.scanInProgress_) {
console.error('Scan-completed event recieved. But scan is not started.');
return;
}
this.updateCommands();
this.hideSpinnerLater_();
this.refreshCurrentDirectoryMetadata_();
// To avoid flickering postpone updating the ui by a small amount of time.
// There is a high chance, that metadata will be received within 50 ms.
this.scanCompletedTimer_ = setTimeout(function() {
// Check if batch updates are already finished by onScanUpdated_().
if (this.scanUpdatedAtLeastOnceOrCompleted_)
return;
this.scanUpdatedAtLeastOnceOrCompleted_ = true;
this.scanInProgress_ = false;
if (this.scanUpdatedTimer_) {
clearTimeout(this.scanUpdatedTimer_);
this.scanUpdatedTimer_ = null;
}
this.table_.list.endBatchUpdates();
this.grid_.endBatchUpdates();
this.updateMiddleBarVisibility_();
this.scanCompletedTimer_ = null;
}.bind(this), 50);
};
/**
* @private
*/
FileManager.prototype.onScanUpdated_ = function() {
if (!this.scanInProgress_) {
console.error('Scan-updated event recieved. But scan is not started.');
return;
}
// We need to hide the spinner only once.
if (this.scanUpdatedAtLeastOnceOrCompleted_ || this.scanUpdatedTimer_)
return;
// Show contents incrementally by finishing batch updated, but only after
// 200ms elapsed, to avoid flickering when it is not necessary.
this.scanUpdatedTimer_ = setTimeout(function() {
// We need to hide the spinner only once.
if (this.scanUpdatedAtLeastOnceOrCompleted_)
return;
if (this.scanCompletedTimer_) {
clearTimeout(this.scanCompletedTimer_);
this.scanCompletedTimer_ = null;
}
this.scanUpdatedAtLeastOnceOrCompleted_ = true;
this.scanInProgress_ = false;
this.hideSpinnerLater_();
this.table_.list.endBatchUpdates();
this.grid_.endBatchUpdates();
this.updateMiddleBarVisibility_();
this.scanUpdatedTimer_ = null;
}.bind(this), 200);
};
/**
* @private
*/
FileManager.prototype.onScanCancelled_ = function() {
if (!this.scanInProgress_) {
console.error('Scan-cancelled event recieved. But scan is not started.');
return;
}
this.updateCommands();
this.hideSpinnerLater_();
if (this.scanCompletedTimer_) {
clearTimeout(this.scanCompletedTimer_);
this.scanCompletedTimer_ = null;
}
if (this.scanUpdatedTimer_) {
clearTimeout(this.scanUpdatedTimer_);
this.scanUpdatedTimer_ = null;
}
// Finish unfinished batch updates.
if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
this.scanUpdatedAtLeastOnceOrCompleted_ = true;
this.scanInProgress_ = false;
this.table_.list.endBatchUpdates();
this.grid_.endBatchUpdates();
this.updateMiddleBarVisibility_();
}
};
/**
* @private
*/
FileManager.prototype.cancelSpinnerTimeout_ = function() {
if (this.showSpinnerTimeout_) {
clearTimeout(this.showSpinnerTimeout_);
this.showSpinnerTimeout_ = null;
}
};
/**
* @private
*/
FileManager.prototype.hideSpinnerLater_ = function() {
this.cancelSpinnerTimeout_();
this.showSpinner_(false);
};
/**
* @param {boolean} on True to show, false to hide.
* @private
*/
FileManager.prototype.showSpinner_ = function(on) {
if (on && this.directoryModel_ && this.directoryModel_.isScanning())
this.spinner_.hidden = false;
if (!on && (!this.directoryModel_ ||
!this.directoryModel_.isScanning() ||
this.directoryModel_.getFileList().length != 0)) {
this.spinner_.hidden = true;
}
};
FileManager.prototype.createNewFolder = function() {
var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
// Find a name that doesn't exist in the data model.
var files = this.directoryModel_.getFileList();
var hash = {};
for (var i = 0; i < files.length; i++) {
var name = files.item(i).name;
// Filtering names prevents from conflicts with prototype's names
// and '__proto__'.
if (name.substring(0, defaultName.length) == defaultName)
hash[name] = 1;
}
var baseName = defaultName;
var separator = '';
var suffix = '';
var index = '';
var advance = function() {
separator = ' (';
suffix = ')';
index++;
};
var current = function() {
return baseName + separator + index + suffix;
};
// Accessing hasOwnProperty is safe since hash properties filtered.
while (hash.hasOwnProperty(current())) {
advance();
}
var self = this;
var list = self.currentList_;
var tryCreate = function() {
self.directoryModel_.createDirectory(current(),
onSuccess, onError);
};
var onSuccess = function(entry) {
metrics.recordUserAction('CreateNewFolder');
list.selectedItem = entry;
self.initiateRename();
};
var onError = function(error) {
self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
util.getFileErrorString(error.code)));
};
tryCreate();
};
/**
* @param {Event} event Click event.
* @private
*/
FileManager.prototype.onDetailViewButtonClick_ = function(event) {
this.setListType(FileManager.ListType.DETAIL);
this.currentList_.focus();
};
/**
* @param {Event} event Click event.
* @private
*/
FileManager.prototype.onThumbnailViewButtonClick_ = function(event) {
this.setListType(FileManager.ListType.THUMBNAIL);
this.currentList_.focus();
};
/**
* KeyDown event handler for the document.
* @param {Event} event Key event.
* @private
*/
FileManager.prototype.onKeyDown_ = function(event) {
if (event.srcElement === this.renameInput_) {
// Ignore keydown handler in the rename input box.
return;
}
switch (util.getKeyModifiers(event) + event.keyCode) {
case 'Ctrl-17': // Ctrl => Show hidden setting
this.setCtrlKeyPressed_(true);
return;
case 'Ctrl-190': // Ctrl-. => Toggle filter files.
this.fileFilter_.setFilterHidden(
!this.fileFilter_.isFilterHiddenOn());
event.preventDefault();
return;
case '27': // Escape => Cancel dialog.
if (this.copyManager_ && this.copyManager_.isRunning()) {
// If there is a copy in progress, ESC will cancel it.
event.preventDefault();
this.copyManager_.requestCancel();
return;
}
if (this.dialogType != DialogType.FULL_PAGE) {
// If there is nothing else for ESC to do, then cancel the dialog.
event.preventDefault();
this.cancelButton_.click();
}
break;
}
};
/**
* KeyUp event handler for the document.
* @param {Event} event Key event.
* @private
*/
FileManager.prototype.onKeyUp_ = function(event) {
if (event.srcElement === this.renameInput_) {
// Ignore keydown handler in the rename input box.
return;
}
switch (util.getKeyModifiers(event) + event.keyCode) {
case '17': // Ctrl => Hide hidden setting
this.setCtrlKeyPressed_(false);
return;
}
};
/**
* KeyDown event handler for the div#list-container element.
* @param {Event} event Key event.
* @private
*/
FileManager.prototype.onListKeyDown_ = function(event) {
if (event.srcElement.tagName == 'INPUT') {
// Ignore keydown handler in the rename input box.
return;
}
switch (util.getKeyModifiers(event) + event.keyCode) {
case '8': // Backspace => Up one directory.
event.preventDefault();
var path = this.getCurrentDirectory();
if (path && !PathUtil.isRootPath(path)) {
var path = path.replace(/\/[^\/]+$/, '');
this.directoryModel_.changeDirectory(path);
}
break;
case '13': // Enter => Change directory or perform default action.
// TODO(dgozman): move directory action to dispatchSelectionAction.
var selection = this.getSelection();
if (selection.totalCount == 1 &&
selection.entries[0].isDirectory &&
this.dialogType != DialogType.SELECT_FOLDER &&
this.dialogType != DialogType.SELECT_UPLOAD_FOLDER) {
event.preventDefault();
this.onDirectoryAction(selection.entries[0]);
} else if (this.dispatchSelectionAction_()) {
event.preventDefault();
}
break;
}
switch (event.keyIdentifier) {
case 'Home':
case 'End':
case 'Up':
case 'Down':
case 'Left':
case 'Right':
// When navigating with keyboard we hide the distracting mouse hover
// highlighting until the user moves the mouse again.
this.setNoHover_(true);
break;
}
};
/**
* Suppress/restore hover highlighting in the list container.
* @param {boolean} on True to temporarity hide hover state.
* @private
*/
FileManager.prototype.setNoHover_ = function(on) {
if (on) {
this.listContainer_.classList.add('nohover');
} else {
this.listContainer_.classList.remove('nohover');
}
};
/**
* KeyPress event handler for the div#list-container element.
* @param {Event} event Key event.
* @private
*/
FileManager.prototype.onListKeyPress_ = function(event) {
if (event.srcElement.tagName == 'INPUT') {
// Ignore keypress handler in the rename input box.
return;
}
if (event.ctrlKey || event.metaKey || event.altKey)
return;
var now = new Date();
var char = String.fromCharCode(event.charCode).toLowerCase();
var text = now - this.textSearchState_.date > 1000 ? '' :
this.textSearchState_.text;
this.textSearchState_ = {text: text + char, date: now};
this.doTextSearch_();
};
/**
* Mousemove event handler for the div#list-container element.
* @param {Event} event Mouse event.
* @private
*/
FileManager.prototype.onListMouseMove_ = function(event) {
// The user grabbed the mouse, restore the hover highlighting.
this.setNoHover_(false);
};
/**
* Performs a 'text search' - selects a first list entry with name
* starting with entered text (case-insensitive).
* @private
*/
FileManager.prototype.doTextSearch_ = function() {
var text = this.textSearchState_.text;
if (!text)
return;
var dm = this.directoryModel_.getFileList();
for (var index = 0; index < dm.length; ++index) {
var name = dm.item(index).name;
if (name.substring(0, text.length).toLowerCase() == text) {
this.currentList_.selectionModel.selectedIndexes = [index];
return;
}
}
this.textSearchState_.text = '';
};
/**
* Handle a click of the cancel button. Closes the window.
* TODO(jamescook): Make unload handler work automatically, crbug.com/104811
*
* @param {Event} event The click event.
* @private
*/
FileManager.prototype.onCancel_ = function(event) {
chrome.fileBrowserPrivate.cancelDialog();
this.onUnload_();
window.close();
};
/**
* Resolves selected file urls returned from an Open dialog.
*
* For drive files this involves some special treatment.
* Starts getting drive files if needed.
*
* @param {Array.<string>} fileUrls Drive URLs.
* @param {function(Array.<string>)} callback To be called with fixed URLs.
* @private
*/
FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) {
if (this.isOnDrive()) {
chrome.fileBrowserPrivate.getDriveFiles(
fileUrls,
function(localPaths) {
callback(fileUrls);
});
} else {
callback(fileUrls);
}
};
/**
* Closes this modal dialog with some files selected.
* TODO(jamescook): Make unload handler work automatically, crbug.com/104811
* @param {Object} selection Contains urls, filterIndex and multiple fields.
* @private
*/
FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) {
var self = this;
function callback() {
self.onUnload_();
window.close();
}
if (selection.multiple) {
chrome.fileBrowserPrivate.selectFiles(
selection.urls, this.params_.shouldReturnLocalPath, callback);
} else {
var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE);
chrome.fileBrowserPrivate.selectFile(
selection.urls[0], selection.filterIndex, forOpening,
this.params_.shouldReturnLocalPath, callback);
}
};
/**
* Tries to close this modal dialog with some files selected.
* Performs preprocessing if needed (e.g. for Drive).
* @param {Object} selection Contains urls, filterIndex and multiple fields.
* @private
*/
FileManager.prototype.selectFilesAndClose_ = function(selection) {
if (!this.isOnDrive() ||
this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
return;
}
var shade = this.document_.createElement('div');
shade.className = 'shade';
var footer = this.dialogDom_.querySelector('.button-panel');
var progress = footer.querySelector('.progress-track');
progress.style.width = '0%';
var cancelled = false;
var progressMap = {};
var filesStarted = 0;
var filesTotal = selection.urls.length;
for (var index = 0; index < selection.urls.length; index++) {
progressMap[selection.urls[index]] = -1;
}
var lastPercent = 0;
var bytesTotal = 0;
var bytesDone = 0;
var onFileTransfersUpdated = function(statusList) {
for (var index = 0; index < statusList.length; index++) {
var status = statusList[index];
var escaped = encodeURI(status.fileUrl);
if (!(escaped in progressMap)) continue;
if (status.total == -1) continue;
var old = progressMap[escaped];
if (old == -1) {
// -1 means we don't know file size yet.
bytesTotal += status.total;
filesStarted++;
old = 0;
}
bytesDone += status.processed - old;
progressMap[escaped] = status.processed;
}
var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
// For files we don't have information about, assume the progress is zero.
percent = percent * filesStarted / filesTotal * 100;
// Do not decrease the progress. This may happen, if first downloaded
// file is small, and the second one is large.
lastPercent = Math.max(lastPercent, percent);
progress.style.width = lastPercent + '%';
}.bind(this);
var setup = function() {
this.document_.querySelector('.dialog-container').appendChild(shade);
setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100);
footer.setAttribute('progress', 'progress');
this.cancelButton_.removeEventListener('click', this.onCancelBound_);
this.cancelButton_.addEventListener('click', onCancel);
chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
onFileTransfersUpdated);
}.bind(this);
var cleanup = function() {
shade.parentNode.removeChild(shade);
footer.removeAttribute('progress');
this.cancelButton_.removeEventListener('click', onCancel);
this.cancelButton_.addEventListener('click', this.onCancelBound_);
chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
onFileTransfersUpdated);
}.bind(this);
var onCancel = function() {
cancelled = true;
// According to API cancel may fail, but there is no proper UI to reflect
// this. So, we just silently assume that everything is cancelled.
chrome.fileBrowserPrivate.cancelFileTransfers(
selection.urls, function(response) {});
cleanup();
}.bind(this);
var onResolved = function(resolvedUrls) {
if (cancelled) return;
cleanup();
selection.urls = resolvedUrls;
// Call next method on a timeout, as it's unsafe to
// close a window from a callback.
setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
}.bind(this);
var onProperties = function(properties) {
for (var i = 0; i < properties.length; i++) {
if (!properties[i] || properties[i].present) {
// For files already in GCache, we don't get any transfer updates.
filesTotal--;
}
}
this.resolveSelectResults_(selection.urls, onResolved);
}.bind(this);
setup();
this.metadataCache_.get(selection.urls, 'drive', onProperties);
};
/**
* Handle a click of the ok button.
*
* The ok button has different UI labels depending on the type of dialog, but
* in code it's always referred to as 'ok'.
*
* @param {Event} event The click event.
* @private
*/
FileManager.prototype.onOk_ = function(event) {
if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
// Save-as doesn't require a valid selection from the list, since
// we're going to take the filename from the text input.
var filename = this.filenameInput_.value;
if (!filename)
throw new Error('Missing filename!');
var directory = this.getCurrentDirectoryEntry();
var currentDirUrl = directory.toURL();
if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
currentDirUrl += '/';
this.validateFileName_(currentDirUrl, filename, function(isValid) {
if (!isValid)
return;
if (util.isFakeDirectoryEntry(directory)) {
// Can't save a file into a fake directory.
return;
}
var selectFileAndClose = function() {
this.selectFilesAndClose_({
urls: [currentDirUrl + encodeURIComponent(filename)],
multiple: false,
filterIndex: this.getSelectedFilterIndex_(filename)
});
}.bind(this);
directory.getFile(
filename, {create: false},
function(entry) {
// An existing file is found. Show confirmation dialog to
// overwrite it. If the user select "OK" on the dialog, save it.
this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
selectFileAndClose);
}.bind(this),
function(error) {
if (error.code == FileError.NOT_FOUND_ERR) {
// The file does not exist, so it should be ok to create a
// new file.
selectFileAndClose();
return;
}
if (error.code == FileError.TYPE_MISMATCH_ERR) {
// An directory is found.
// Do not allow to overwrite directory.
this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
return;
}
// Unexpected error.
console.error('File save failed: ' + error.code);
}.bind(this));
}.bind(this));
return;
}
var files = [];
var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
if ((this.dialogType == DialogType.SELECT_FOLDER ||
this.dialogType == DialogType.SELECT_UPLOAD_FOLDER) &&
selectedIndexes.length == 0) {
var url = this.getCurrentDirectoryURL();
var singleSelection = {
urls: [url],
multiple: false,
filterIndex: this.getSelectedFilterIndex_()
};
this.selectFilesAndClose_(singleSelection);
return;
}
// All other dialog types require at least one selected list item.
// The logic to control whether or not the ok button is enabled should
// prevent us from ever getting here, but we sanity check to be sure.
if (!selectedIndexes.length)
throw new Error('Nothing selected!');
var dm = this.directoryModel_.getFileList();
for (var i = 0; i < selectedIndexes.length; i++) {
var entry = dm.item(selectedIndexes[i]);
if (!entry) {
console.error('Error locating selected file at index: ' + i);
continue;
}
files.push(entry.toURL());
}
// Multi-file selection has no other restrictions.
if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
var multipleSelection = {
urls: files,
multiple: true
};
this.selectFilesAndClose_(multipleSelection);
return;
}
// Everything else must have exactly one.
if (files.length > 1)
throw new Error('Too many files selected!');
var selectedEntry = dm.item(selectedIndexes[0]);
if (this.dialogType == DialogType.SELECT_FOLDER ||
this.dialogType == DialogType.SELECT_UPLOAD_FOLDER) {
if (!selectedEntry.isDirectory)
throw new Error('Selected entry is not a folder!');
} else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
if (!selectedEntry.isFile)
throw new Error('Selected entry is not a file!');
}
var singleSelection = {
urls: [files[0]],
multiple: false,
filterIndex: this.getSelectedFilterIndex_()
};
this.selectFilesAndClose_(singleSelection);
};
/**
* Verifies the user entered name for file or folder to be created or
* renamed to. Name restrictions must correspond to File API restrictions
* (see DOMFilePath::isValidPath). Curernt WebKit implementation is
* out of date (spec is
* http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to
* be fixed. Shows message box if the name is invalid.
*
* It also verifies if the name length is in the limit of the filesystem.
*
* @param {string} parentUrl The URL of the parent directory entry.
* @param {string} name New file or folder name.
* @param {function} onDone Function to invoke when user closes the
* warning box or immediatelly if file name is correct. If the name was
* valid it is passed true, and false otherwise.
* @private
*/
FileManager.prototype.validateFileName_ = function(parentUrl, name, onDone) {
var msg;
var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
if (testResult) {
msg = strf('ERROR_INVALID_CHARACTER', testResult[0]);
} else if (/^\s*$/i.test(name)) {
msg = str('ERROR_WHITESPACE_NAME');
} else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) {
msg = str('ERROR_RESERVED_NAME');
} else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') {
msg = str('ERROR_HIDDEN_NAME');
}
if (msg) {
this.alert.show(msg, function() {
onDone(false);
});
return;
}
var self = this;
chrome.fileBrowserPrivate.validatePathNameLength(
parentUrl, name, function(valid) {
if (!valid) {
self.alert.show(str('ERROR_LONG_NAME'),
function() { onDone(false); });
} else {
onDone(true);
}
});
};
/**
* Handler invoked on preference setting in drive context menu.
*
* @param {string} pref The preference to alter.
* @param {boolean} inverted Invert the value if true.
* @param {Event} event The click event.
* @private
*/
FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) {
var newValue = !event.target.hasAttribute('checked');
if (newValue)
event.target.setAttribute('checked', 'checked');
else
event.target.removeAttribute('checked');
var changeInfo = {};
changeInfo[pref] = inverted ? !newValue : newValue;
chrome.fileBrowserPrivate.setPreferences(changeInfo);
};
/**
* Invoked when the search box is changed.
*
* @param {Event} event The changed event.
* @private
*/
FileManager.prototype.onSearchBoxUpdate_ = function(event) {
var searchString = this.searchBox_.value;
this.updateSearchBoxStyles_();
if (this.isOnDrive()) {
// When the search text is changed, finishes the search and showes back
// the last directory by passing an empty string to
// {@code DirectoryModel.search()}.
if (this.directoryModel_.isSearching() &&
this.lastSearchQuery_ != searchString) {
this.doSearch('');
}
// On drive, incremental search is not invoked since we have an auto-
// complete suggestion instead.
return;
}
this.search_(searchString);
};
/**
* Handles special keys such as Escape on the search box.
*
* @param {Event} event The keydown event.
* @private
*/
FileManager.prototype.onSearchBoxKeyDown_ = function(event) {
// Handle only Esc key now.
if (event.keyCode != 27) return;
if (this.searchBox_.value) return;
var currentList = this.listType_ == FileManager.ListType.DETAIL ?
this.table_.list : this.grid_;
currentList.focus();
if (currentList.dataModel.length != 0 &&
currentList.selectionModel.selectedIndex == -1) {
currentList.selectionModel.selectedIndex = 0;
}
};
/**
* Updates search box's CSS classes.
* These classes are refered from CSS.
*
* @private
*/
FileManager.prototype.updateSearchBoxStyles_ = function() {
var TEXT_BOX_PADDING = 16; // in px.
this.searchBoxWrapper_.classList.toggle('has-text',
!!this.searchBox_.value);
var width = this.searchTextMeasure_.getWidth(this.searchBox_.value) +
TEXT_BOX_PADDING;
this.searchBox_.style.width = width + 'px';
};
/**
* Search files and update the list with the search result.
*
* @param {string} searchString String to be searched with.
* @private
*/
FileManager.prototype.search_ = function(searchString) {
var noResultsDiv = this.document_.getElementById('no-search-results');
var reportEmptySearchResults = function() {
if (this.directoryModel_.getFileList().length === 0) {
// The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
// hence we escapes |searchString| here.
var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
util.htmlEscape(searchString));
noResultsDiv.innerHTML = html;
noResultsDiv.setAttribute('show', 'true');
} else {
noResultsDiv.removeAttribute('show');
}
};
var hideNoResultsDiv = function() {
noResultsDiv.removeAttribute('show');
};
this.doSearch(searchString,
reportEmptySearchResults.bind(this),
hideNoResultsDiv.bind(this));
};
/**
* Performs search and displays results.
*
* @param {string} query Query that will be searched for.
* @param {function()=} opt_onSearchRescan Function that will be called when
* the search directory is rescanned (i.e. search results are displayed).
* @param {function()=} opt_onClearSearch Function to be called when search
* state gets cleared.
*/
FileManager.prototype.doSearch = function(
searchString, opt_onSearchRescan, opt_onClearSearch) {
var onSearchRescan = opt_onSearchRescan || function() {};
var onClearSearch = opt_onClearSearch || function() {};
this.lastSearchQuery_ = searchString;
this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
};
/**
* Requests autocomplete suggestions for files on Drive.
* Once the suggestions are returned, the autocomplete popup will show up.
*
* @param {string} query The text to autocomplete from.
* @private
*/
FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
query = query.trimLeft();
// Only Drive supports auto-compelete
if (!this.isOnDrive())
return;
// Remember the most recent query. If there is an other request in progress,
// then it's result will be discarded and it will call a new request for
// this query.
this.lastAutocompleteQuery_ = query;
if (this.autocompleteSuggestionsBusy_)
return;
// The autocomplete list should be resized and repositioned here as the
// search box is resized when it's focused.
this.autocompleteList_.syncWidthAndPositionToInput();
if (!query) {
this.autocompleteList_.suggestions = [];
return;
}
var headerItem = {isHeaderItem: true, searchQuery: query};
if (!this.autocompleteList_.dataModel ||
this.autocompleteList_.dataModel.length == 0)
this.autocompleteList_.suggestions = [headerItem];
else
// Updates only the head item to prevent a flickering on typing.
this.autocompleteList_.dataModel.splice(0, 1, headerItem);
this.autocompleteSuggestionsBusy_ = true;
var searchParams = {
'query': query,
'types': 'ALL',
'maxResults': 4
};
chrome.fileBrowserPrivate.searchDriveMetadata(
searchParams,
function(suggestions) {
this.autocompleteSuggestionsBusy_ = false;
// Discard results for previous requests and fire a new search
// for the most recent query.
if (query != this.lastAutocompleteQuery_) {
this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
return;
}
// Keeps the items in the suggestion list.
this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
}.bind(this));
};
/**
* Creates a ListItem element for autocomple.
*
* @param {Object} item An object representing a suggestion.
* @return {HTMLElement} Element containing the autocomplete suggestions.
* @private
*/
FileManager.prototype.createAutocompleteListItem_ = function(item) {
var li = new cr.ui.ListItem();
li.itemInfo = item;
var icon = this.document_.createElement('div');
icon.className = 'detail-icon';
var text = this.document_.createElement('div');
text.className = 'detail-text';
if (item.isHeaderItem) {
icon.setAttribute('search-icon');
text.innerHTML =
strf('SEARCH_DRIVE_HTML', util.htmlEscape(item.searchQuery));
} else {
var iconType = FileType.getIcon(item.entry);
icon.setAttribute('file-type-icon', iconType);
// highlightedBaseName is a piece of HTML with meta characters properly
// escaped. See the comment at fileBrowserPrivate.searchDriveMetadata().
text.innerHTML = item.highlightedBaseName;
}
li.appendChild(icon);
li.appendChild(text);
return li;
};
/**
* Opens the currently selected suggestion item.
* @private
*/
FileManager.prototype.openAutocompleteSuggestion_ = function() {
var selectedItem = this.autocompleteList_.selectedItem;
// If the entry is the search item or no entry is selected, just change to
// the search result.
if (!selectedItem || selectedItem.isHeaderItem) {
var query = selectedItem ?
selectedItem.searchQuery : this.searchBox_.value;
this.search_(query);
return;
}
var entry = selectedItem.entry;
// If the entry is a directory, just change the directory.
if (entry.isDirectory) {
this.onDirectoryAction(entry);
return;
}
var urls = [entry.toURL()];
var self = this;
// To open a file, first get the mime type.
this.metadataCache_.get(urls, 'drive', function(props) {
var mimeType = props[0].contentMimeType || '';
var mimeTypes = [mimeType];
var openIt = function() {
if (self.dialogType == DialogType.FULL_PAGE) {
var tasks = new FileTasks(self);
tasks.init(urls, mimeTypes);
tasks.executeDefault();
} else {
self.onOk_();
}
};
// Change the current directory to the directory that contains the
// selected file. Note that this is necessary for an image or a video,
// which should be opened in the gallery mode, as the gallery mode
// requires the entry to be in the current directory model. For
// consistency, the current directory is always changed regardless of
// the file type.
entry.getParent(function(parent) {
var onDirectoryChanged = function(event) {
self.directoryModel_.removeEventListener('scan-completed',
onDirectoryChanged);
self.directoryModel_.selectEntry(entry.name);
openIt();
}
// changeDirectory() returns immediately. We should wait until the
// directory scan is complete.
self.directoryModel_.addEventListener('scan-completed',
onDirectoryChanged);
self.directoryModel_.changeDirectory(
parent.fullPath,
function() {
// Remove the listner if the change directory failed.
self.directoryModel_.removeEventListener('scan-completed',
onDirectoryChanged);
});
});
});
};
/**
* Opens the default app change dialog.
*/
FileManager.prototype.showChangeDefaultAppPicker = function() {
var onActionsReady = function(actions, rememberedActionId) {
var items = [];
var defaultIndex = -1;
for (var i = 0; i < actions.length; i++) {
if (actions[i].hidden)
continue;
var title = actions[i].title;
if (actions[i].id == rememberedActionId) {
title += ' ' + loadTimeData.getString('DEFAULT_ACTION_LABEL');
defaultIndex = i;
}
var item = {
id: actions[i].id,
label: title,
class: actions[i].class,
iconUrl: actions[i].icon100
};
items.push(item);
}
this.defaultTaskPicker.show(
str('CHANGE_DEFAULT_APP_BUTTON_LABEL'),
'',
items,
defaultIndex,
function(action) {
ActionChoiceUtil.setRememberedActionId(action.id);
});
}.bind(this);
ActionChoiceUtil.getDefinedActions(loadTimeData, function(actions) {
ActionChoiceUtil.getRememberedActionId(function(actionId) {
onActionsReady(actions, actionId);
});
});
};
FileManager.prototype.decorateSplitter = function(splitterElement) {
var self = this;
var Splitter = cr.ui.Splitter;
var customSplitter = cr.ui.define('div');
customSplitter.prototype = {
__proto__: Splitter.prototype,
handleSplitterDragStart: function(e) {
Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
this.ownerDocument.documentElement.classList.add('col-resize');
},
handleSplitterDragMove: function(deltaX) {
Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
self.onResize_();
},
handleSplitterDragEnd: function(e) {
Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
this.ownerDocument.documentElement.classList.remove('col-resize');
}
};
customSplitter.decorate(splitterElement);
};
/**
* Updates default action menu item to match passed taskItem (icon,
* label and action).
*
* @param {Object} defaultItem - taskItem to match.
* @param {boolean} isMultiple - if multiple tasks available.
*/
FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
isMultiple) {
if (defaultItem) {
if (defaultItem.iconType) {
this.defaultActionMenuItem_.style.backgroundImage = '';
this.defaultActionMenuItem_.setAttribute('file-type-icon',
defaultItem.iconType);
} else if (defaultItem.iconUrl) {
this.defaultActionMenuItem_.style.backgroundImage =
'url(' + defaultItem.iconUrl + ')';
} else {
this.defaultActionMenuItem_.style.backgroundImage = '';
}
this.defaultActionMenuItem_.label = defaultItem.title;
this.defaultActionMenuItem_.taskId = defaultItem.taskId;
}
var defaultActionSeparator =
this.dialogDom_.querySelector('#default-action-separator');
this.openWithCommand_.canExecuteChange();
this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
this.defaultActionMenuItem_.hidden = !defaultItem;
defaultActionSeparator.hidden = !defaultItem;
};
/**
* Window beforeunload handler.
* @return {string} Message to show. Ignored when running as a packaged app.
* @private
*/
FileManager.prototype.onBeforeUnload_ = function() {
if (this.filePopup_ &&
this.filePopup_.contentWindow &&
this.filePopup_.contentWindow.beforeunload) {
// The gallery might want to prevent the unload if it is busy.
return this.filePopup_.contentWindow.beforeunload();
}
return null;
};
/**
* @return {FileSelection} Selection object.
*/
FileManager.prototype.getSelection = function() {
return this.selectionHandler_.selection;
};
/**
* @return {ArrayDataModel} File list.
*/
FileManager.prototype.getFileList = function() {
return this.directoryModel_.getFileList();
};
/**
* @return {cr.ui.List} Current list object.
*/
FileManager.prototype.getCurrentList = function() {
return this.currentList_;
};
/**
* Retrieve the preferences of the files.app. This method caches the result
* and returns it unless opt_update is true.
* @param {function(Object.<string, *>)} callback Callback to get the
* preference.
* @param {boolean=} opt_update If is's true, don't use the cache and
* retrieve latest preference. Default is false.
* @private
*/
FileManager.prototype.getPreferences_ = function(callback, opt_update) {
if (!opt_update && this.preferences_ !== undefined) {
callback(this.preferences_);
return;
}
chrome.fileBrowserPrivate.getPreferences(function(prefs) {
this.preferences_ = prefs;
callback(prefs);
}.bind(this));
};
/**
* Set the flag expressing whether the ctrl key is pressed or not.
* @param {boolean} flag New value of the flag
* @private
*/
FileManager.prototype.setCtrlKeyPressed_ = function(flag) {
this.ctrlKeyPressed_ = flag;
// Before the DOM is constructed, the key event can be handled.
var cacheClearCommand =
this.document_.querySelector('#drive-clear-local-cache');
if (cacheClearCommand)
cacheClearCommand.canExecuteChange();
};
})();