blob: 5ef0eb0c0e23fe06b1192d568ea92f120212e622 [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.
/**
* The current selection object.
* @param {!Array<number>} indexes
* @param {!Array<Entry>} entries
* @constructor
* @struct
*/
function FileSelection(indexes, entries) {
/**
* @public {!Array<number>}
* @const
*/
this.indexes = indexes;
/**
* @public {!Array<!Entry>}
* @const
*/
this.entries = entries;
/**
* @public {!Array<string>}
*/
this.mimeTypes = [];
/**
* @public {number}
*/
this.totalCount = 0;
/**
* @public {number}
*/
this.fileCount = 0;
/**
* @public {number}
*/
this.directoryCount = 0;
/**
* @public {boolean}
*/
this.anyFilesNotInCache = true;
/**
* @public {boolean}
*/
this.anyFilesHosted = true;
/**
* @public {?string}
*/
this.iconType = null;
/**
* @private {Promise<boolean>}
*/
this.additionalPromise_ = null;
entries.forEach(function(entry) {
if (this.iconType == null) {
this.iconType = FileType.getIcon(entry);
} else if (this.iconType != 'unknown') {
var iconType = FileType.getIcon(entry);
if (this.iconType != iconType)
this.iconType = 'unknown';
}
if (entry.isFile) {
this.fileCount += 1;
} else {
this.directoryCount += 1;
}
this.totalCount++;
}.bind(this));
}
FileSelection.prototype.computeAdditional = function(metadataModel) {
if (!this.additionalPromise_) {
this.additionalPromise_ =
metadataModel
.get(
this.entries,
constants.FILE_SELECTION_METADATA_PREFETCH_PROPERTY_NAMES)
.then(function(props) {
var present = props.filter(function(p) {
// If no availableOffline property, then assume it's available.
return !('availableOffline' in p) || p.availableOffline;
});
const hosted = props.filter(function(p) {
return p.hosted;
});
this.anyFilesNotInCache = present.length !== props.length;
this.anyFilesHosted = !!hosted.length;
this.mimeTypes = props.map(function(value) {
return value.contentMimeType || '';
});
return true;
}.bind(this));
}
return this.additionalPromise_;
};
/**
* This object encapsulates everything related to current selection.
*
* @param {!DirectoryModel} directoryModel
* @param {!FileOperationManager} fileOperationManager
* @param {!ListContainer} listContainer
* @param {!MetadataModel} metadataModel
* @param {!VolumeManager} volumeManager
* @param {!AllowedPaths} allowedPaths
* @extends {cr.EventTarget}
* @constructor
* @struct
*/
function FileSelectionHandler(
directoryModel, fileOperationManager, listContainer, metadataModel,
volumeManager, allowedPaths) {
cr.EventTarget.call(this);
/**
* @private {DirectoryModel}
* @const
*/
this.directoryModel_ = directoryModel;
/**
* @private {ListContainer}
* @const
*/
this.listContainer_ = listContainer;
/**
* @private {MetadataModel}
* @const
*/
this.metadataModel_ = metadataModel;
/**
* @private {VolumeManager}
* @const
*/
this.volumeManager_ = volumeManager;
/**
* @type {FileSelection}
*/
this.selection = new FileSelection([], []);
/**
* @private {?number}
*/
this.selectionUpdateTimer_ = 0;
/**
* @private {number}
*/
this.lastFileSelectionTime_ = Date.now();
/**
* @private {AllowedPaths}
* @const
*/
this.allowedPaths_ = allowedPaths;
util.addEventListenerToBackgroundComponent(
assert(fileOperationManager), 'entries-changed',
this.onFileSelectionChanged.bind(this));
// Register evnets to update file selections.
directoryModel.addEventListener(
'directory-changed', this.onFileSelectionChanged.bind(this));
}
/**
* @enum {string}
*/
FileSelectionHandler.EventType = {
/**
* Dispatched every time when selection is changed.
*/
CHANGE: 'change',
/**
* Dispatched |UPDATE_DELAY| ms after the selecton is changed.
* If multiple changes are happened during the term, only one CHANGE_THROTTLED
* event is dispatched.
*/
CHANGE_THROTTLED: 'changethrottled'
};
/**
* Delay in milliseconds before recalculating the selection in case the
* selection is changed fast, or there are many items. Used to avoid freezing
* the UI.
* @const {number}
*/
FileSelectionHandler.UPDATE_DELAY = 200;
/**
* Number of items in the selection which triggers the update delay. Used to
* let the Material Design animations complete before performing a heavy task
* which would cause the UI freezing.
* @const {number}
*/
FileSelectionHandler.NUMBER_OF_ITEMS_HEAVY_TO_COMPUTE = 100;
/**
* FileSelectionHandler extends cr.EventTarget.
*/
FileSelectionHandler.prototype.__proto__ = cr.EventTarget.prototype;
/**
* Update the UI when the selection model changes.
*/
FileSelectionHandler.prototype.onFileSelectionChanged = function() {
var indexes = this.listContainer_.selectionModel.selectedIndexes;
var entries = indexes.map(function(index) {
return /** @type {!Entry} */ (
this.directoryModel_.getFileList().item(index));
}.bind(this));
this.selection = new FileSelection(indexes, entries);
if (this.selectionUpdateTimer_) {
clearTimeout(this.selectionUpdateTimer_);
this.selectionUpdateTimer_ = null;
}
// The rest of the selection properties are computed via (sometimes lengthy)
// asynchronous calls. We initiate these calls after a timeout. If the
// selection is changing quickly we only do this once when it slows down.
var updateDelay = FileSelectionHandler.UPDATE_DELAY;
var now = Date.now();
if (now > (this.lastFileSelectionTime_ || 0) + updateDelay &&
indexes.length < FileSelectionHandler.NUMBER_OF_ITEMS_HEAVY_TO_COMPUTE) {
// The previous selection change happened a while ago and there is few
// selected items, so computation is lightweight. Update the UI without
// delay.
updateDelay = 0;
}
this.lastFileSelectionTime_ = now;
var selection = this.selection;
this.selectionUpdateTimer_ = setTimeout(function() {
this.selectionUpdateTimer_ = null;
if (this.selection === selection)
this.updateFileSelectionAsync_(selection);
}.bind(this), updateDelay);
cr.dispatchSimpleEvent(this, FileSelectionHandler.EventType.CHANGE);
};
/**
* Calculates async selection stats and updates secondary UI elements.
*
* @param {FileSelection} selection The selection object.
* @private
*/
FileSelectionHandler.prototype.updateFileSelectionAsync_ = function(selection) {
if (this.selection !== selection)
return;
// Calculate all additional and heavy properties.
selection.computeAdditional(this.metadataModel_).then(() => {
if (this.selection !== selection)
return;
cr.dispatchSimpleEvent(
this, FileSelectionHandler.EventType.CHANGE_THROTTLED);
});
};
/**
* Returns true if all files in the selection files are selectable.
* @return {boolean}
*/
FileSelectionHandler.prototype.isAvailable = function() {
if (!this.directoryModel_.isOnDrive()) {
return true;
}
return !(
this.isOfflineWithUncachedFilesSelected_() ||
this.isDialogWithHostedFilesSelected_());
};
/**
* Returns true if we're offline with any selected files absent from the cache.
* @return {boolean}
* @private
*/
FileSelectionHandler.prototype.isOfflineWithUncachedFilesSelected_ =
function() {
return this.volumeManager_.getDriveConnectionState().type ===
VolumeManagerCommon.DriveConnectionType.OFFLINE &&
this.selection.anyFilesNotInCache;
};
/**
* Returns true if we're a dialog requiring real files with hosted files
* selected.
* @return {boolean}
* @private
*/
FileSelectionHandler.prototype.isDialogWithHostedFilesSelected_ = function() {
return this.allowedPaths_ !== AllowedPaths.ANY_PATH_OR_URL &&
this.selection.anyFilesHosted;
};