blob: d473c74529fe5c1e8c69425c4723705c70cc249b [file] [log] [blame]
// Copyright (c) 2013 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.
// Namespace
const directorytree = {};
////////////////////////////////////////////////////////////////////////////////
// DirectoryTreeBase
/**
* Implementation of methods for DirectoryTree and DirectoryItem. These classes
* inherits cr.ui.Tree/TreeItem so we can't make them inherit this class.
* Instead, we separate their implementations to this separate object and call
* it with setting 'this' from DirectoryTree/Item.
*/
const DirectoryItemTreeBaseMethods = {};
/**
* Finds an item by entry and returns it.
* @param {!Entry} entry
* @return {DirectoryItem} null is returned if it's not found.
* @this {(DirectoryItem|DirectoryTree)}
*/
DirectoryItemTreeBaseMethods.getItemByEntry = function(entry) {
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (!item.entry) {
continue;
}
if (util.isSameEntry(item.entry, entry)) {
// The Drive root volume item "Google Drive" and its child "My Drive" have
// the same entry. When we look for a tree item of Drive's root directory,
// "My Drive" should be returned, as we use "Google Drive" for grouping
// "My Drive", "Shared with me", "Recent", and "Offline".
// Therefore, we have to skip "Google Drive" here.
if (item instanceof DriveVolumeItem) {
return item.getItemByEntry(entry);
}
return item;
}
// Team drives are descendants of the Drive root volume item "Google Drive".
// When we looking for an item in team drives, recursively search inside the
// "Google Drive" root item.
if (util.isTeamDriveEntry(entry) && item instanceof DriveVolumeItem) {
return item.getItemByEntry(entry);
}
if (util.isComputersEntry(entry) && item instanceof DriveVolumeItem) {
return item.getItemByEntry(entry);
}
if (util.isDescendantEntry(item.entry, entry)) {
return item.getItemByEntry(entry);
}
}
return null;
};
/**
* Finds a parent directory of the {@code entry} in {@code this}, and
* invokes the DirectoryItem.selectByEntry() of the found directory.
*
* @param {!DirectoryEntry|!FakeEntry} entry The entry to be searched for. Can
* be a fake.
* @return {boolean} True if the parent item is found.
* @this {(DirectoryItem|VolumeItem|DirectoryTree)}
*/
DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (!item.entry) {
continue;
}
// Team drives are descendants of the Drive root volume item "Google Drive".
// When we looking for an item in team drives, recursively search inside the
// "Google Drive" root item.
if (util.isTeamDriveEntry(entry) && item instanceof DriveVolumeItem) {
item.selectByEntry(entry);
return true;
}
if (util.isComputersEntry(entry) && item instanceof DriveVolumeItem) {
item.selectByEntry(entry);
return true;
}
if (util.isDescendantEntry(item.entry, entry) ||
util.isSameEntry(item.entry, entry)) {
item.selectByEntry(entry);
return true;
}
}
return false;
};
/**
* Records UMA for the selected entry at {@code location}. Records slightly
* differently if the expand icon is selected and {@code expandIconSelected} is
* true.
*
* @param {Event} e The click event.
* @param {VolumeManagerCommon.RootType} rootType The root type to record.
* @param {boolean} isRootEntry Whether the entry selected was a root entry.
* @return
*/
DirectoryItemTreeBaseMethods.recordUMASelectedEntry = function(
e, rootType, isRootEntry) {
const expandIconSelected = e.target.classList.contains('expand-icon');
let metricName = 'Location.OnEntrySelected.TopLevel';
if (!expandIconSelected && isRootEntry) {
metricName = 'Location.OnEntrySelected.TopLevel';
} else if (!expandIconSelected && !isRootEntry) {
metricName = 'Location.OnEntrySelected.NonTopLevel';
} else if (expandIconSelected && isRootEntry) {
metricName = 'Location.OnEntryExpandedOrCollapsed.TopLevel';
} else if (expandIconSelected && !isRootEntry) {
metricName = 'Location.OnEntryExpandedOrCollapsed.NonTopLevel';
}
metrics.recordEnum(
metricName, rootType, VolumeManagerCommon.RootTypesForUMA);
};
Object.freeze(DirectoryItemTreeBaseMethods);
const TREE_ITEM_INNER_HTML = '<div class="tree-row">' +
' <paper-ripple fit class="recenteringTouch"></paper-ripple>' +
' <span class="expand-icon"></span>' +
' <span class="icon"></span>' +
' <span class="label entry-name"></span>' +
'</div>' +
'<div class="tree-children"></div>';
////////////////////////////////////////////////////////////////////////////////
// DirectoryItem
/**
* An expandable directory in the tree. Each element represents one folder (sub
* directory) or one volume (root directory).
*
* @param {string} label Label for this item.
* @param {DirectoryTree} tree Current tree, which contains this item.
* @extends {cr.ui.TreeItem}
* @constructor
*/
function DirectoryItem(label, tree) {
const item = /** @type {DirectoryItem} */ (new cr.ui.TreeItem());
// Get the original label id defined by TreeItem, before overwriting
// prototype.
const labelId = item.labelElement.id;
item.__proto__ = DirectoryItem.prototype;
if (window.IN_TEST) {
item.setAttribute('dir-type', 'DirectoryItem');
item.setAttribute('entry-label', label);
}
item.parentTree_ = tree;
item.directoryModel_ = tree.directoryModel;
item.fileFilter_ = tree.directoryModel.getFileFilter();
item.innerHTML = TREE_ITEM_INNER_HTML;
item.labelElement.id = labelId;
item.addEventListener('expand', item.onExpand_.bind(item), false);
// Listen for collapse because for the delayed expansion case all
// children are also collapsed.
item.addEventListener('collapse', item.onCollapse_.bind(item), false);
// Default delayExpansion to false. Volumes will set it to true for
// provided file systems. SubDirectories will inherit from their
// parent.
item.delayExpansion = false;
// Sets hasChildren=false tentatively. This will be overridden after
// scanning sub-directories in updateSubElementsFromList().
item.hasChildren = false;
item.label = label;
// @type {!Array<Entry>} Filled after updateSubDirectories read entries.
item.entries_ = [];
// @type {function()=} onMetadataUpdated_ bound to |this| used to listen
// metadata update events.
item.onMetadataUpdateBound_ = undefined;
return item;
}
DirectoryItem.prototype = {
__proto__: cr.ui.TreeItem.prototype,
/**
* The DirectoryEntry corresponding to this DirectoryItem. This may be
* a dummy DirectoryEntry.
* @type {DirectoryEntry|Object}
*/
get entry() {
return null;
},
/**
* The element containing the label text and the icon.
* @type {!HTMLElement}
* @override
*/
get labelElement() {
return this.firstElementChild.querySelector('.label');
},
/**
* Returns true if this item is inside any part of My Drive.
* @type {!boolean}
*/
get insideMyDrive() {
if (!this.entry) {
return false;
}
const locationInfo =
this.parentTree_.volumeManager.getLocationInfo(this.entry);
return locationInfo &&
locationInfo.rootType === VolumeManagerCommon.RootType.DRIVE;
},
/**
* Returns true if this item is inside any part of Computers.
* @type {!boolean}
*/
get insideComputers() {
if (!this.entry) {
return false;
}
const locationInfo =
this.parentTree_.volumeManager.getLocationInfo(this.entry);
return locationInfo &&
(locationInfo.rootType ===
VolumeManagerCommon.RootType.COMPUTERS_GRAND_ROOT ||
locationInfo.rootType === VolumeManagerCommon.RootType.COMPUTER);
},
/**
* Returns true if this item is inside any part of Drive, including Team
* Drive.
* @type {!boolean}
*/
get insideDrive() {
if (!this.entry) {
return false;
}
const locationInfo =
this.parentTree_.volumeManager.getLocationInfo(this.entry);
return locationInfo &&
(locationInfo.rootType === VolumeManagerCommon.RootType.DRIVE ||
locationInfo.rootType ===
VolumeManagerCommon.RootType.TEAM_DRIVES_GRAND_ROOT ||
locationInfo.rootType === VolumeManagerCommon.RootType.TEAM_DRIVE ||
locationInfo.rootType ===
VolumeManagerCommon.RootType.COMPUTERS_GRAND_ROOT ||
locationInfo.rootType === VolumeManagerCommon.RootType.COMPUTER ||
locationInfo.rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE ||
locationInfo.rootType ===
VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME ||
locationInfo.rootType ===
VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT);
},
/**
* If the this directory supports 'shared' feature, as in displays shared
* icon. It's only supported inside 'My Drive', even Team Drive doesn't
* support it.
* @type {!boolean}
*/
get supportDriveSpecificIcons() {
return this.insideMyDrive || this.insideComputers;
},
};
/**
* Handles the Metadata update event. It updates the shared icon of this item
* sub-folders.
* @param {Event} event Metadata update event.
*/
DirectoryItem.prototype.onMetadataUpdated_ = function(event) {
if (!this.supportDriveSpecificIcons) {
return;
}
const updateableProperties = ['shared', 'isMachineRoot', 'isExternalMedia'];
if (!updateableProperties.some((prop) => event.names.has(prop))) {
return;
}
let index = 0;
while (this.entries_[index]) {
const childEntry = this.entries_[index];
const childElement = this.items[index];
if (event.entriesMap.has(childEntry.toURL())) {
childElement.updateDriveSpecificIcons();
}
index++;
}
};
/**
* Updates sub-elements of {@code this} reading {@code DirectoryEntry}.
* The list of {@code DirectoryEntry} are not updated by this method.
*
* @param {boolean} recursive True if the all visible sub-directories are
* updated recursively including left arrows. If false, the update walks
* only immediate child directories without arrows.
* @this {DirectoryItem}
*/
DirectoryItem.prototype.updateSubElementsFromList = function(recursive) {
let index = 0;
const tree = this.parentTree_;
let item;
while (this.entries_[index]) {
const currentEntry = this.entries_[index];
const currentElement = this.items[index];
const label =
util.getEntryLabel(
tree.volumeManager_.getLocationInfo(currentEntry), currentEntry) ||
'';
if (index >= this.items.length) {
// If currentEntry carries its navigationModel we generate an item
// accordingly. Used for Crostini when displayed within My Files.
if (currentEntry.navigationModel) {
item = DirectoryTree.createDirectoryItem(
currentEntry.navigationModel, tree);
} else {
item = new SubDirectoryItem(label, currentEntry, this, tree);
}
this.add(item);
index++;
} else if (util.isSameEntry(currentEntry, currentElement.entry)) {
currentElement.updateDriveSpecificIcons();
if (recursive && this.expanded) {
if (this.delayExpansion) {
// Only update deeper on expanded children.
if (currentElement.expanded) {
currentElement.updateSubDirectories(true /* recursive */);
}
// Show the expander even without knowing if there are children.
currentElement.mayHaveChildren_ = true;
} else {
currentElement.updateSubDirectories(true /* recursive */);
}
}
index++;
} else if (currentEntry.toURL() < currentElement.entry.toURL()) {
// If currentEntry carries its navigationModel we generate an item
// accordingly. Used for Crostini when displayed within My Files.
if (currentEntry.navigationModel) {
item = DirectoryTree.createDirectoryItem(
currentEntry.navigationModel, tree);
} else {
item = new SubDirectoryItem(label, currentEntry, this, tree);
}
this.addAt(item, index);
index++;
} else if (currentEntry.toURL() > currentElement.entry.toURL()) {
this.remove(currentElement);
}
}
let removedChild;
while (removedChild = this.items[index]) {
this.remove(removedChild);
}
if (index === 0) {
this.hasChildren = false;
this.expanded = false;
} else {
this.hasChildren = true;
}
};
/**
* Calls DirectoryItemTreeBaseMethods.getItemByEntry().
* @param {!Entry} entry
* @return {DirectoryItem}
*/
DirectoryItem.prototype.getItemByEntry = function(entry) {
return DirectoryItemTreeBaseMethods.getItemByEntry.call(this, entry);
};
/**
* Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
*
* @param {!DirectoryEntry|!FakeEntry} entry The entry to be searched for. Can
* be a fake.
* @return {boolean} True if the parent item is found.
*/
DirectoryItem.prototype.searchAndSelectByEntry = function(entry) {
return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
};
/**
* Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
* a complex layout. This call is not necessary, so we are ignoring it.
*
* @param {boolean=} opt_unused Unused.
* @override
*/
DirectoryItem.prototype.scrollIntoViewIfNeeded = function(opt_unused) {};
/**
* Removes the child node, but without selecting the parent item, to avoid
* unintended changing of directories. Removing is done externally, and other
* code will navigate to another directory.
*
* @param {!cr.ui.TreeItem=} child The tree item child to remove.
* @override
*/
DirectoryItem.prototype.remove = function(child) {
this.lastElementChild.removeChild(/** @type {!cr.ui.TreeItem} */(child));
if (this.items.length == 0) {
this.hasChildren = false;
}
};
/**
* Removes the has-children attribute which allows returning
* to the ambiguous may-have-children state.
*/
DirectoryItem.prototype.clearHasChildren = function() {
const rowItem = this.firstElementChild;
this.removeAttribute('has-children');
rowItem.removeAttribute('has-children');
};
/**
* Invoked when the item is being expanded.
* @param {!Event} e Event.
* @private
*/
DirectoryItem.prototype.onExpand_ = function(e) {
if (this.supportDriveSpecificIcons && !this.onMetadataUpdateBound_) {
this.onMetadataUpdateBound_ = this.onMetadataUpdated_.bind(this);
this.parentTree_.metadataModel_.addEventListener(
'update', this.onMetadataUpdateBound_);
}
this.updateSubDirectories(
true /* recursive */,
() => {
if (!this.insideDrive) {
return;
}
this.parentTree_.metadataModel_.get(
this.entries_,
constants.LIST_CONTAINER_METADATA_PREFETCH_PROPERTY_NAMES);
},
() => {
this.expanded = false;
});
e.stopPropagation();
};
/**
* Invoked when the item is being collapsed.
* @param {!Event} e Event.
* @private
*/
DirectoryItem.prototype.onCollapse_ = function(e) {
if (this.onMetadataUpdateBound_) {
this.parentTree_.metadataModel_.removeEventListener(
'update', this.onMetadataUpdateBound_);
this.onMetadataUpdateBound_ = undefined;
}
if (this.delayExpansion) {
// For file systems where it is performance intensive
// to update recursively when items expand this proactively
// collapses all children to avoid having to traverse large
// parts of the tree when reopened.
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (item.expanded) {
item.expanded = false;
}
}
}
e.stopPropagation();
};
/**
* Invoked when the tree item is clicked.
*
* @param {Event} e Click event.
* @override
*/
DirectoryItem.prototype.handleClick = function(e) {
cr.ui.TreeItem.prototype.handleClick.call(this, e);
if (!this.entry || e.button === 2) {
return;
}
if (!e.target.classList.contains('expand-icon')) {
this.directoryModel_.activateDirectoryEntry(this.entry);
}
// If this is DriveVolumeItem, the UMA has already been recorded.
if (!(this instanceof DriveVolumeItem)) {
const location = this.tree.volumeManager.getLocationInfo(this.entry);
DirectoryItemTreeBaseMethods.recordUMASelectedEntry.call(
this, e, location.rootType, location.isRootEntry);
}
};
/**
* Default sorting for DirectoryItem sub-dirrectories.
* @param {!Array<!Entry>} entries Entries to be sorted.
* @returns {!Array<!Entry>}
*/
DirectoryItem.prototype.sortEntries = function(entries) {
entries.sort(util.compareName);
const filter = this.fileFilter_.filter.bind(this.fileFilter_);
return entries.filter(filter);
};
/**
* Retrieves the latest subdirectories and update them on the tree.
* @param {boolean} recursive True if the update is recursively.
* @param {function()=} opt_successCallback Callback called on success.
* @param {function()=} opt_errorCallback Callback called on error.
*/
DirectoryItem.prototype.updateSubDirectories = function(
recursive, opt_successCallback, opt_errorCallback) {
if (!this.entry || this.entry.createReader === undefined) {
opt_errorCallback && opt_errorCallback();
return;
}
const onSuccess = (entries) => {
this.entries_ = entries;
this.updateSubElementsFromList(recursive);
opt_successCallback && opt_successCallback();
};
const reader = this.entry.createReader();
const entries = [];
const readEntry = () => {
reader.readEntries((results) => {
if (!results.length) {
onSuccess(this.sortEntries(entries));
return;
}
for (let i = 0; i < results.length; i++) {
const entry = results[i];
if (entry.isDirectory) {
entries.push(entry);
}
}
readEntry();
});
};
readEntry();
};
/**
* Searches for the changed directory in the current subtree, and if it is found
* then updates it.
*
* @param {!DirectoryEntry} changedDirectoryEntry The entry ot the changed
* directory.
*/
DirectoryItem.prototype.updateItemByEntry = function(changedDirectoryEntry) {
if (util.isSameEntry(changedDirectoryEntry, this.entry)) {
this.updateSubDirectories(false /* recursive */);
return;
}
// Traverse the entire subtree to find the changed element.
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (!item.entry) {
continue;
}
if (util.isDescendantEntry(item.entry, changedDirectoryEntry) ||
util.isSameEntry(item.entry, changedDirectoryEntry)) {
item.updateItemByEntry(changedDirectoryEntry);
break;
}
}
};
/**
* Update the icon based on whether the folder is shared on Drive.
*/
DirectoryItem.prototype.updateDriveSpecificIcons = function() {};
/**
* Select the item corresponding to the given {@code entry}.
* @param {!DirectoryEntry|!FakeEntry} entry The entry to be selected. Can be a
* fake.
*/
DirectoryItem.prototype.selectByEntry = function(entry) {
if (util.isSameEntry(entry, this.entry)) {
this.selected = true;
return;
}
if (this.searchAndSelectByEntry(entry)) {
return;
}
// If the entry doesn't exist, updates sub directories and tries again.
this.updateSubDirectories(
false /* recursive */,
this.searchAndSelectByEntry.bind(this, entry));
};
/**
* Executes the assigned action as a drop target.
*/
DirectoryItem.prototype.doDropTargetAction = function() {
this.expanded = true;
};
/**
* Change current directory to the entry of this item.
*/
DirectoryItem.prototype.activate = function() {
if (this.entry) {
this.parentTree_.directoryModel.activateDirectoryEntry(this.entry);
}
};
/**
* Set up eject button if needed.
* @param {HTMLElement} rowElement The parent element for eject button.
* @private
*/
DirectoryItem.prototype.setupEjectButton_ = function(rowElement) {
const ejectButton = cr.doc.createElement('button');
// Block other mouse handlers.
ejectButton.addEventListener('mouseup', (event) => {
event.stopPropagation();
});
ejectButton.addEventListener('up', (event) => {
event.stopPropagation();
});
ejectButton.addEventListener('mousedown', (event) => {
event.stopPropagation();
});
ejectButton.addEventListener('down', (event) => {
event.stopPropagation();
});
ejectButton.className = 'root-eject';
ejectButton.setAttribute('aria-label', str('UNMOUNT_DEVICE_BUTTON_LABEL'));
ejectButton.setAttribute('tabindex', '0');
ejectButton.addEventListener('click', (event) => {
event.stopPropagation();
const unmountCommand = cr.doc.querySelector('command#unmount');
// Let's make sure 'canExecute' state of the command is properly set for
// the root before executing it.
unmountCommand.canExecuteChange(this);
unmountCommand.execute(this);
});
rowElement.appendChild(ejectButton);
// Add paper-ripple effect on the eject button.
const ripple = cr.doc.createElement('paper-ripple');
ripple.setAttribute('fit', '');
ripple.className = 'circle recenteringTouch';
ejectButton.appendChild(ripple);
};
////////////////////////////////////////////////////////////////////////////////
// SubDirectoryItem
/**
* A sub directory in the tree. Each element represents a directory which is not
* a volume's root.
*
* @param {string} label Label for this item.
* @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
* @param {DirectoryItem|ShortcutItem|DirectoryTree} parentDirItem
* Parent of this item.
* @param {DirectoryTree} tree Current tree, which contains this item.
* @extends {DirectoryItem}
* @constructor
*/
function SubDirectoryItem(label, dirEntry, parentDirItem, tree) {
const item = new DirectoryItem(label, tree);
item.__proto__ = SubDirectoryItem.prototype;
if (window.IN_TEST) {
item.setAttribute('dir-type', 'SubDirectoryItem');
}
item.entry = dirEntry;
item.delayExpansion = parentDirItem.delayExpansion;
if (item.delayExpansion) {
item.clearHasChildren();
item.mayHaveChildren_ = true;
}
// Sets up icons of the item.
const icon = item.querySelector('.icon');
icon.classList.add('item-icon');
const location = tree.volumeManager.getLocationInfo(item.entry);
if (location && location.rootType && location.isRootEntry) {
icon.setAttribute('volume-type-icon', location.rootType);
if (window.IN_TEST && location.volumeInfo) {
item.setAttribute(
'volume-type-for-testing', location.volumeInfo.volumeType);
item.setAttribute('drive-label', location.volumeInfo.driveLabel);
}
} else {
const rootType = location.rootType || null;
const iconOverride = FileType.getIconOverrides(dirEntry, rootType);
// Add Downloads icon as volume so current test code passes with
// MyFilesVolume flag enabled and disabled.
if (iconOverride) {
icon.setAttribute('volume-type-icon', iconOverride);
}
icon.setAttribute('file-type-icon', iconOverride || 'folder');
item.updateDriveSpecificIcons();
}
// Sets up context menu of the item.
if (tree.contextMenuForSubitems) {
cr.ui.contextMenuHandler.setContextMenu(item, tree.contextMenuForSubitems);
}
// Populates children now if needed.
if (parentDirItem.expanded) {
item.updateSubDirectories(false /* recursive */);
}
return item;
}
SubDirectoryItem.prototype = {
__proto__: DirectoryItem.prototype,
get entry() {
return this.dirEntry_;
},
set entry(value) {
this.dirEntry_ = value;
// Set helper attribute for testing.
if (window.IN_TEST) {
this.setAttribute('full-path-for-testing', this.dirEntry_.fullPath);
}
}
};
/**
* Update the icon based on whether the folder is shared on Drive.
* @override
*/
SubDirectoryItem.prototype.updateDriveSpecificIcons = function() {
const icon = this.querySelector('.icon');
const metadata = this.parentTree_.metadataModel.getCache(
[this.dirEntry_], ['shared', 'isMachineRoot', 'isExternalMedia']);
icon.classList.toggle('shared', !!(metadata[0] && metadata[0].shared));
if (metadata[0] && metadata[0].isMachineRoot) {
icon.setAttribute(
'volume-type-icon', VolumeManagerCommon.RootType.COMPUTER);
}
if (metadata[0] && metadata[0].isExternalMedia) {
icon.setAttribute(
'volume-type-icon', VolumeManagerCommon.RootType.EXTERNAL_MEDIA);
}
};
/**
* A directory of entries. Each element represents an entry.
*
* @param {VolumeManagerCommon.RootType} rootType The root type to record.
* @param {!NavigationModelFakeItem} modelItem NavigationModelItem of this
* volume.
* @param {DirectoryTree} tree Current tree, which contains this item.
* @extends {DirectoryItem}
* @constructor
*/
function EntryListItem(rootType, modelItem, tree) {
const item = new DirectoryItem(modelItem.label, tree);
// Get the original label id defined by TreeItem, before overwriting
// prototype.
item.__proto__ = EntryListItem.prototype;
if (window.IN_TEST) {
item.setAttribute('dir-type', 'EntryListItem');
}
item.entries_ = [];
item.rootType_ = rootType;
item.modelItem_ = modelItem;
item.dirEntry_ = modelItem.entry;
item.parentTree_ = tree;
if (rootType === VolumeManagerCommon.RootType.REMOVABLE) {
item.setupEjectButton_(item.rowElement);
}
const icon = queryRequiredElement('.icon', item);
if (window.IN_TEST && item.entry && item.entry.volumeInfo) {
item.setAttribute(
'volume-type-for-testing', item.entry.volumeInfo.volumeType);
// TODO(crbug.com/880130) Remove volume-type-icon from here once
// MyFilesVolume flag is removed.
icon.setAttribute('volume-type-icon', rootType);
}
icon.classList.add('item-icon');
icon.setAttribute('root-type-icon', rootType);
return item;
}
EntryListItem.prototype = {
__proto__: DirectoryItem.prototype,
/**
* The DirectoryEntry corresponding to this DirectoryItem. This may be
* a dummy DirectoryEntry.
* @type {DirectoryEntry|Object}
*/
get entry() {
return this.dirEntry_;
},
/**
* The element containing the label text and the icon.
* @type {!HTMLElement}
* @override
*/
get labelElement() {
return this.firstElementChild.querySelector('.label');
},
/**
* @type {!NavigationModelVolumeItem}
*/
get modelItem() {
return this.modelItem_;
}
};
/**
* Default sorting for DirectoryItem sub-dirrectories.
* @param {!Array<!Entry>} entries Entries to be sorted.
* @returns {!Array<!Entry>}
*/
EntryListItem.prototype.sortEntries = function(entries) {
if (!util.isMyFilesVolumeEnabled()) {
return DirectoryItem.prototype.sortEntries.apply(this, [entries]);
}
// If the root entry hasn't been resolved yet.
if (!this.entry) {
return DirectoryItem.prototype.sortEntries.apply(this, [entries]);
}
// Use locationInfo from first entry because it only compare within the same
// volume.
const locationInfo =
this.parentTree_.volumeManager_.getLocationInfo(entries[0]);
const compareFunction = util.compareLabelAndGroupBottomEntries(
locationInfo, this.entry.getUIChildren());
const filter = this.fileFilter_.filter.bind(this.fileFilter_);
return entries.filter(filter).sort(compareFunction);
};
/**
* Retrieves the subdirectories and update them on the tree. Runs synchronously,
* since EntryList has its subdirectories already in memory.
* @param {boolean} recursive True if the update is recursively.
* @param {function()=} opt_successCallback Callback called on success.
* @param {function()=} opt_errorCallback Callback called on error.
*/
EntryListItem.prototype.updateSubDirectories = function(
recursive, opt_successCallback, opt_errorCallback) {
if (!this.entry || this.entry.createReader === undefined) {
opt_errorCallback && opt_errorCallback();
return;
}
this.entries_ = [];
const onSuccess = (entries) => {
this.entries_ = entries;
this.updateSubElementsFromList(recursive);
if (this.entries_.length > 0) {
this.expanded = true;
}
opt_successCallback && opt_successCallback();
};
const reader = this.entry.createReader();
const entries = [];
const readEntry = () => {
reader.readEntries((results) => {
if (!results.length) {
onSuccess(this.sortEntries(entries));
return;
}
for (let i = 0; i < results.length; i++) {
const entry = results[i];
if (entry.isDirectory) {
entries.push(entry);
}
}
readEntry();
});
};
readEntry();
};
////////////////////////////////////////////////////////////////////////////////
// VolumeItem
/**
* A TreeItem which represents a volume. Volume items are displayed as
* top-level children of DirectoryTree.
*
* @param {!NavigationModelVolumeItem} modelItem NavigationModelItem of this
* volume.
* @param {!DirectoryTree} tree Current tree, which contains this item.
* @extends {DirectoryItem}
* @constructor
*/
function VolumeItem(modelItem, tree) {
const item = /** @type {VolumeItem} */ (
new DirectoryItem(modelItem.volumeInfo.label, tree));
item.__proto__ = VolumeItem.prototype;
item.modelItem_ = modelItem;
item.volumeInfo_ = modelItem.volumeInfo;
// Provided volumes should delay the expansion of child nodes
// for performance reasons.
item.delayExpansion = (item.volumeInfo.volumeType === 'provided');
// Set helper attribute for testing.
if (window.IN_TEST) {
item.setAttribute('volume-type-for-testing', item.volumeInfo_.volumeType);
item.setAttribute('dir-type', 'VolumeItem');
item.setAttribute('drive-label', item.volumeInfo_.driveLabel);
}
item.setupIcon_(item.querySelector('.icon'), item.volumeInfo_);
// Attach a placeholder for rename input text box and the eject icon if the
// volume is ejectable
if ((modelItem.volumeInfo_.source === VolumeManagerCommon.Source.DEVICE &&
modelItem.volumeInfo_.volumeType !==
VolumeManagerCommon.VolumeType.MTP) ||
modelItem.volumeInfo_.source === VolumeManagerCommon.Source.FILE) {
// This placeholder is added to allow to put textbox before eject button
// while executing renaming action on external drive.
item.setupRenamePlaceholder_(item.rowElement);
item.setupEjectButton_(item.rowElement);
}
// Sets up context menu of the item.
if (tree.contextMenuForRootItems) {
item.setContextMenu_(tree.contextMenuForRootItems);
}
// Populate children of this volume using resolved display root.
item.volumeInfo_.resolveDisplayRoot((displayRoot) => {
item.updateSubDirectories(false /* recursive */);
});
return item;
}
VolumeItem.prototype = {
__proto__: DirectoryItem.prototype,
/**
* Directory entry for the display root, whose initial value is null.
* @type {DirectoryEntry}
* @override
*/
get entry() {
return this.volumeInfo_.displayRoot;
},
/**
* @type {!VolumeInfo}
*/
get volumeInfo() {
return this.volumeInfo_;
},
/**
* @type {!NavigationModelVolumeItem}
*/
get modelItem() {
return this.modelItem_;
}
};
/**
* Sets the context menu for volume items.
* @param {!cr.ui.Menu} menu Menu to be set.
* @private
*/
VolumeItem.prototype.setContextMenu_ = function(menu) {
cr.ui.contextMenuHandler.setContextMenu(this, menu);
};
/**
* @override
*/
VolumeItem.prototype.updateSubDirectories = function(
recursive, opt_successCallback, opt_errorCallback) {
if (this.volumeInfo.volumeType ===
VolumeManagerCommon.VolumeType.MEDIA_VIEW) {
// If this is a media-view volume, we don't show child directories.
// (Instead, we provide flattend files in the file list.)
opt_successCallback && opt_successCallback();
} else {
DirectoryItem.prototype.updateSubDirectories.call(
this, recursive, opt_successCallback, opt_errorCallback);
}
};
/**
* Change current entry to this volume's root directory.
* @override
*/
VolumeItem.prototype.activate = function() {
const directoryModel = this.parentTree_.directoryModel;
const onEntryResolved = (entry) => {
// Changes directory to the model item's root directory if needed.
if (!util.isSameEntry(directoryModel.getCurrentDirEntry(), entry)) {
metrics.recordUserAction('FolderShortcut.Navigate');
directoryModel.changeDirectoryEntry(entry);
}
// In case of failure in resolveDisplayRoot() in the volume's constructor,
// update the volume's children here.
this.updateSubDirectories(false);
};
this.volumeInfo_.resolveDisplayRoot(
onEntryResolved,
() => {
// Error, the display root is not available. It may happen on Drive.
this.parentTree_.dataModel.onItemNotFoundError(this.modelItem);
});
};
/**
* Set up icon of this volume item.
* @param {Element} icon Icon element to be setup.
* @param {VolumeInfo} volumeInfo VolumeInfo determines the icon type.
* @private
*/
VolumeItem.prototype.setupIcon_ = function(icon, volumeInfo) {
icon.classList.add('item-icon');
const backgroundImage =
util.iconSetToCSSBackgroundImageValue(volumeInfo.iconSet);
if (backgroundImage !== 'none') {
// The icon div is not yet added to DOM, therefore it is impossible to
// use style.backgroundImage.
icon.setAttribute(
'style', 'background-image: ' + backgroundImage);
}
icon.setAttribute('volume-type-icon', volumeInfo.volumeType);
if (volumeInfo.volumeType === VolumeManagerCommon.VolumeType.MEDIA_VIEW) {
icon.setAttribute(
'volume-subtype',
VolumeManagerCommon.getMediaViewRootTypeFromVolumeId(
volumeInfo.volumeId));
} else {
icon.setAttribute('volume-subtype', volumeInfo.deviceType || '');
}
};
/**
* Set up rename input textbox placeholder if needed.
* @param {HTMLElement} rowElement The parent element for placeholder.
* @private
*/
VolumeItem.prototype.setupRenamePlaceholder_ = function(rowElement) {
const placeholder = cr.doc.createElement('span');
placeholder.className = 'rename-placeholder';
rowElement.appendChild(placeholder);
};
////////////////////////////////////////////////////////////////////////////////
// DriveVolumeItem
/**
* A TreeItem which represents a Drive volume. Drive volume has fake entries
* such as Team Drives, Shared with me, and Offline in it.
*
* @param {!NavigationModelVolumeItem} modelItem NavigationModelItem of this
* volume.
* @param {!DirectoryTree} tree Current tree, which contains this item.
* @extends {VolumeItem}
* @constructor
*/
function DriveVolumeItem(modelItem, tree) {
const item = new VolumeItem(modelItem, tree);
item.__proto__ = DriveVolumeItem.prototype;
item.classList.add('drive-volume');
if (window.IN_TEST) {
item.setAttribute('dir-type', 'DriveVolumeItem');
}
return item;
}
DriveVolumeItem.prototype = {
__proto__: VolumeItem.prototype,
// Overrides the property 'expanded' to prevent Drive volume from shrinking.
get expanded() {
return Object.getOwnPropertyDescriptor(
cr.ui.TreeItem.prototype, 'expanded').get.call(this);
},
set expanded(b) {
Object.getOwnPropertyDescriptor(
cr.ui.TreeItem.prototype, 'expanded').set.call(this, b);
// When Google Drive is expanded while it is selected, select the My Drive.
if (b) {
if (this.selected && this.entry) {
this.selectByEntry(this.entry);
}
}
}
};
/**
* Invoked when the tree item is clicked.
*
* @param {Event} e Click event.
* @override
*/
DriveVolumeItem.prototype.handleClick = function(e) {
VolumeItem.prototype.handleClick.call(this, e);
this.selectDisplayRoot_(e.target);
DirectoryItemTreeBaseMethods.recordUMASelectedEntry.call(
this, e, VolumeManagerCommon.RootType.DRIVE_FAKE_ROOT, true);
};
/**
* Creates Team Drives root if there is any team drive, if there is no team
* drive, then it removes the root.
*
* Since we don't currently support any functionality with just the grand root
* (e.g. you can't create a new team drive from the root yet), remove/don't
* create the grand root so it can't be reached via keyboard.
* If there is at least one Team Drive, add/show the Team Drives grand root.
*
* @return {!Promise<SubDirectoryItem>} Resolved with Team Drive Grand Root
* SubDirectoryItem instance, or undefined when it shouldn't exist.
* @private
*/
DriveVolumeItem.prototype.createTeamDrivesGrandRoot_ = function() {
return new Promise((resolve) => {
const teamDriveGrandRoot = this.volumeInfo_.teamDriveDisplayRoot;
if (!teamDriveGrandRoot) {
// Team Drive is disabled.
resolve();
return;
}
let index;
for (let i = 0; i < this.items.length; i++) {
const entry = this.items[i] && this.items[i].entry;
if (entry && util.isSameEntry(entry, teamDriveGrandRoot)) {
index = i;
break;
}
}
const reader = teamDriveGrandRoot.createReader();
reader.readEntries((results) => {
metrics.recordSmallCount('TeamDrivesCount', results.length);
// Only create grand root if there is at least 1 child/result.
if (results.length) {
if (index !== undefined) {
this.items[index].hidden = false;
resolve(this.items[index]);
return;
}
// Create if it doesn't exist yet.
const label = util.getEntryLabel(
this.parentTree_.volumeManager_.getLocationInfo(
teamDriveGrandRoot),
teamDriveGrandRoot) ||
'';
const item = new SubDirectoryItem(
label, teamDriveGrandRoot, this, this.parentTree_);
this.addAt(item, 1);
item.updateSubDirectories(false);
resolve(item);
return;
} else {
// When there is no team drive, the grand root should be removed.
if (index && this.items[index].parentItem) {
this.items[index].parentItem.remove(this.items[index]);
}
resolve();
return;
}
});
});
};
/**
* Creates Computers root if there is any computer. If there is no computer,
* then it removes the root.
*
* Since we don't currently support any functionality with just the grand root
* (e.g. you can't create a new computer from the root yet), remove/don't
* create the grand root so it can't be reached via keyboard.
* If there is at least one Computer, add/show the Computer grand root.
*
* @return {!Promise<SubDirectoryItem>} Resolved with Computer Grand Root
* SubDirectoryItem instance, or undefined when it shouldn't exist.
* @private
*/
DriveVolumeItem.prototype.createComputersGrandRoot_ = function() {
return new Promise((resolve) => {
const computerGrandRoot = this.volumeInfo_.computersDisplayRoot;
if (!computerGrandRoot) {
// Computer is disabled.
resolve();
return;
}
let index;
for (let i = 0; i < this.items.length; i++) {
const entry = this.items[i] && this.items[i].entry;
if (entry && util.isSameEntry(entry, computerGrandRoot)) {
index = i;
break;
}
}
const reader = computerGrandRoot.createReader();
reader.readEntries((results) => {
metrics.recordSmallCount('ComputersCount', results.length);
// Only create grand root if there is at least 1 child/result.
if (results.length) {
if (index !== undefined) {
this.items[index].hidden = false;
resolve(this.items[index]);
return;
}
// Create if it doesn't exist yet.
const label = util.getEntryLabel(
this.parentTree_.volumeManager_.getLocationInfo(
computerGrandRoot),
computerGrandRoot) ||
'';
const item = new SubDirectoryItem(
label, computerGrandRoot, this, this.parentTree_);
// We want to show "Computers" after "Team Drives", the
// computersIndexPosition_() helper function will work out the correct
// index to place "Computers" at.
const position = this.computersIndexPosition_();
this.addAt(item, position);
item.updateSubDirectories(false);
resolve(item);
return;
} else {
// When there is no computer, the grand root should be removed.
if (index && this.items[index].parentItem) {
this.items[index].parentItem.remove(this.items[index]);
}
resolve();
return;
}
});
});
};
/**
* Change current entry to the entry corresponding to My Drive.
*/
DriveVolumeItem.prototype.activate = function() {
VolumeItem.prototype.activate.call(this);
this.selectDisplayRoot_(this);
};
/**
* Select Drive's display root.
* @param {EventTarget} target The event target.
*/
DriveVolumeItem.prototype.selectDisplayRoot_ = function(target) {
if (!target.classList.contains('expand-icon')) {
// If the Drive volume is clicked, select one of the children instead of
// this item itself.
this.volumeInfo_.resolveDisplayRoot((displayRoot) => {
this.searchAndSelectByEntry(displayRoot);
});
}
};
/**
* Retrieves the latest subdirectories and update them on the tree.
* @param {boolean} recursive True if the update is recursively.
* @override
*/
DriveVolumeItem.prototype.updateSubDirectories = function(recursive) {
if (!this.entry || this.hasChildren) {
return;
}
let entries = [this.entry];
const teamDrivesDisplayRoot = this.volumeInfo_.teamDriveDisplayRoot;
if (!!teamDrivesDisplayRoot) {
entries.push(teamDrivesDisplayRoot);
}
const computersDisplayRoot = this.volumeInfo_.computersDisplayRoot;
if (!!computersDisplayRoot) {
entries.push(computersDisplayRoot);
}
// Drive volume has children including fake entries (offline, recent, ...)
const fakeEntries = [];
if (this.parentTree_.fakeEntriesVisible_) {
for (const key in this.volumeInfo_.fakeEntries) {
fakeEntries.push(this.volumeInfo_.fakeEntries[key]);
}
// This list is sorted by URL on purpose.
fakeEntries.sort((a, b) => {
if (a.toURL() === b.toURL()) {
return 0;
}
return b.toURL() > a.toURL() ? 1 : -1;
});
entries = entries.concat(fakeEntries);
}
for (let i = 0; i < entries.length; i++) {
// Only create the team drives root if there is at least 1 team drive.
const entry = entries[i];
if (entry === teamDrivesDisplayRoot) {
this.createTeamDrivesGrandRoot_();
} else if (entry === computersDisplayRoot) {
this.createComputersGrandRoot_();
} else {
const label =
util.getEntryLabel(
this.parentTree_.volumeManager_.getLocationInfo(entry), entry) ||
'';
const item = new SubDirectoryItem(label, entry, this, this.parentTree_);
this.add(item);
item.updateSubDirectories(false);
}
}
};
/**
* Searches for the changed directory in the current subtree, and if it is found
* then updates it.
*
* @param {!DirectoryEntry} changedDirectoryEntry The entry ot the changed
* directory.
* @override
*/
DriveVolumeItem.prototype.updateItemByEntry = function(changedDirectoryEntry) {
const isTeamDriveChild = util.isTeamDriveEntry(changedDirectoryEntry);
// If Team Drive grand root has been removed and we receive an update for an
// team drive, we need to create the Team Drive grand root.
if (isTeamDriveChild) {
this.createTeamDrivesGrandRoot_().then((teamDriveGrandRootItem) => {
if (teamDriveGrandRootItem) {
teamDriveGrandRootItem.updateItemByEntry(changedDirectoryEntry);
}
});
return;
}
const isComputersChild = util.isComputersEntry(changedDirectoryEntry);
// If Computers grand root has been removed and we receive an update for an
// computer, we need to create the Computers grand root.
if (isComputersChild) {
this.createComputersGrandRoot_().then((computersGrandRootItem) => {
if (computersGrandRootItem) {
computersGrandRootItem.updateItemByEntry(changedDirectoryEntry);
}
});
return;
}
// Must be under "My Drive", which is always the first item.
this.items[0].updateItemByEntry(changedDirectoryEntry);
};
/**
* Select the item corresponding to the given entry.
* @param {!DirectoryEntry|!FakeEntry} entry The directory entry to be selected.
* Can be a fake.
* @override
*/
DriveVolumeItem.prototype.selectByEntry = function(entry) {
// Find the item to be selected among children.
this.searchAndSelectByEntry(entry);
};
/**
* Return the index where we want to display the "Computers" root.
* @private
*/
DriveVolumeItem.prototype.computersIndexPosition_ = function() {
// We want the order to be
// - My Drive
// - Team Drives (if the user has any)
// - Computers (if the user has any)
// So if the user has team drives we want index position 2, otherwise index
// position 1.
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (!item.entry) {
continue;
}
if (util.isTeamDriveEntry(item.entry)) {
return 2;
}
}
return 1;
};
////////////////////////////////////////////////////////////////////////////////
// ShortcutItem
/**
* A TreeItem which represents a shortcut for Drive folder.
* Shortcut items are displayed as top-level children of DirectoryTree.
*
* @param {!NavigationModelShortcutItem} modelItem NavigationModelItem of this
* volume.
* @param {!DirectoryTree} tree Current tree, which contains this item.
* @extends {cr.ui.TreeItem}
* @constructor
*/
function ShortcutItem(modelItem, tree) {
const item = /** @type {ShortcutItem} */ (new cr.ui.TreeItem());
// Get the original label id defined by TreeItem, before overwriting
// prototype.
const labelId = item.labelElement.id;
item.__proto__ = ShortcutItem.prototype;
if (window.IN_TEST) {
item.setAttribute('dir-type', 'ShortcutItem');
}
item.parentTree_ = tree;
item.dirEntry_ = modelItem.entry;
item.modelItem_ = modelItem;
item.innerHTML = TREE_ITEM_INNER_HTML;
item.labelElement.id = labelId;
const icon = item.querySelector('.icon');
icon.classList.add('item-icon');
icon.setAttribute('volume-type-icon', 'shortcut');
if (tree.contextMenuForRootItems) {
item.setContextMenu_(tree.contextMenuForRootItems);
}
item.label = modelItem.entry.name;
// Set the 'label' attribute of this element so it can be selected by tests.
// TODO(sashab): Figure out a reliable way to select elements in the directory
// tree by label and remove this.
item.setAttribute('label', item.label);
return item;
}
ShortcutItem.prototype = {
__proto__: cr.ui.TreeItem.prototype,
get entry() {
return this.dirEntry_;
},
get modelItem() {
return this.modelItem_;
},
get labelElement() {
return this.firstElementChild.querySelector('.label');
}
};
/**
* Finds a parent directory of the {@code entry} in {@code this}, and
* invokes the DirectoryItem.selectByEntry() of the found directory.
*
* @param {!DirectoryEntry|!FakeEntry} entry The entry to be searched for. Can
* be a fake.
* @return {boolean} True if the parent item is found.
*/
ShortcutItem.prototype.searchAndSelectByEntry = function(entry) {
// Always false as shortcuts have no children.
return false;
};
/**
* Invoked when the tree item is clicked.
*
* @param {Event} e Click event.
* @override
*/
ShortcutItem.prototype.handleClick = function(e) {
cr.ui.TreeItem.prototype.handleClick.call(this, e);
// Do not activate with right click.
if (e.button === 2) {
return;
}
this.activate();
// Resets file selection when a volume is clicked.
this.parentTree_.directoryModel.clearSelection();
const location = this.tree.volumeManager.getLocationInfo(this.entry);
DirectoryItemTreeBaseMethods.recordUMASelectedEntry.call(
this, e, location.rootType, location.isRootEntry);
};
/**
* Select the item corresponding to the given entry.
* @param {!DirectoryEntry} entry The directory entry to be selected.
*/
ShortcutItem.prototype.selectByEntry = function(entry) {
if (util.isSameEntry(entry, this.entry)) {
this.selected = true;
}
};
/**
* Sets the context menu for shortcut items.
* @param {!cr.ui.Menu} menu Menu to be set.
* @private
*/
ShortcutItem.prototype.setContextMenu_ = function(menu) {
cr.ui.contextMenuHandler.setContextMenu(this, menu);
};
/**
* Change current entry to the entry corresponding to this shortcut.
*/
ShortcutItem.prototype.activate = function() {
const directoryModel = this.parentTree_.directoryModel;
const onEntryResolved = (entry) => {
// Changes directory to the model item's root directory if needed.
if (!util.isSameEntry(directoryModel.getCurrentDirEntry(), entry)) {
metrics.recordUserAction('FolderShortcut.Navigate');
directoryModel.changeDirectoryEntry(entry);
}
};
// For shortcuts we already have an Entry, but it has to be resolved again
// in case, it points to a non-existing directory.
window.webkitResolveLocalFileSystemURL(
this.entry.toURL(),
onEntryResolved,
() => {
// Error, the entry can't be re-resolved. It may happen for shortcuts
// which targets got removed after resolving the Entry during
// initialization.
this.parentTree_.dataModel.onItemNotFoundError(this.modelItem);
});
};
////////////////////////////////////////////////////////////////////////////////
// FakeItem
/**
* FakeItem is used by Recent and Linux files.
* @param {!VolumeManagerCommon.RootType} rootType root type.
* @param {!NavigationModelFakeItem} modelItem
* @param {!DirectoryTree} tree Current tree, which contains this item.
* @extends {cr.ui.TreeItem}
* @constructor
*/
function FakeItem(rootType, modelItem, tree) {
const item = new cr.ui.TreeItem();
// Get the original label id defined by TreeItem, before overwriting
// prototype.
const labelId = item.labelElement.id;
item.__proto__ = FakeItem.prototype;
if (window.IN_TEST) {
item.setAttribute('dir-type', 'FakeItem');
item.setAttribute('entry-label', modelItem.label);
}
item.rootType_ = rootType;
item.parentTree_ = tree;
item.modelItem_ = modelItem;
item.dirEntry_ = modelItem.entry;
item.innerHTML = TREE_ITEM_INNER_HTML;
item.labelElement.id = labelId;
item.label = modelItem.label;
item.directoryModel_ = tree.directoryModel;
const icon = queryRequiredElement('.icon', item);
icon.classList.add('item-icon');
icon.setAttribute('root-type-icon', rootType);
return item;
}
FakeItem.prototype = {
__proto__: cr.ui.TreeItem.prototype,
get entry() {
return this.dirEntry_;
},
get modelItem() {
return this.modelItem_;
},
get labelElement() {
return this.firstElementChild.querySelector('.label');
}
};
/**
* @param {!DirectoryEntry|!FakeEntry} entry
* @return {boolean} True if the parent item is found.
*/
FakeItem.prototype.searchAndSelectByEntry = function(entry) {
return false;
};
/**
* @override
*/
FakeItem.prototype.handleClick = function(e) {
this.activate();
DirectoryItemTreeBaseMethods.recordUMASelectedEntry.call(
this, e, this.rootType_, true);
};
/**
* @param {!DirectoryEntry} entry
*/
FakeItem.prototype.selectByEntry = function(entry) {
if (util.isSameEntry(entry, this.entry)) {
this.selected = true;
}
};
/**
* Executes the command.
*/
FakeItem.prototype.activate = function() {
this.parentTree_.directoryModel.activateDirectoryEntry(this.entry);
};
/**
* FakeItem doesn't really have sub-directories, it's defined here only to have
* the same API of other Items on this file.
*/
FakeItem.prototype.updateSubDirectories = function(
recursive, opt_successCallback, opt_errorCallback) {
return opt_successCallback && opt_successCallback();
};
/**
* FakeItem doesn't really have shared status/icon so we define here as no-op.
*/
FakeItem.prototype.updateDriveSpecificIcons = function() {};
////////////////////////////////////////////////////////////////////////////////
// DirectoryTree
/**
* Tree of directories on the middle bar. This element is also the root of
* items, in other words, this is the parent of the top-level items.
*
* @constructor
* @extends {cr.ui.Tree}
*/
function DirectoryTree() {}
/**
* Decorates an element.
* @param {HTMLElement} el Element to be DirectoryTree.
* @param {!DirectoryModel} directoryModel Current DirectoryModel.
* @param {!VolumeManager} volumeManager VolumeManager of the system.
* @param {!MetadataModel} metadataModel Shared MetadataModel instance.
* @param {!FileOperationManager} fileOperationManager
* @param {boolean} fakeEntriesVisible True if it should show the fakeEntries.
*/
DirectoryTree.decorate = function(
el, directoryModel, volumeManager, metadataModel, fileOperationManager,
fakeEntriesVisible) {
el.__proto__ = DirectoryTree.prototype;
/** @type {DirectoryTree} */ (el).decorateDirectoryTree(
directoryModel, volumeManager, metadataModel, fileOperationManager,
fakeEntriesVisible);
};
DirectoryTree.prototype = {
__proto__: cr.ui.Tree.prototype,
// DirectoryTree is always expanded.
get expanded() {
return true;
},
/**
* @param {boolean} value Not used.
*/
set expanded(value) {},
/**
* The DirectoryEntry corresponding to this DirectoryItem. This may be
* a dummy DirectoryEntry.
* @type {DirectoryEntry|Object}
*/
get entry() {
return this.dirEntry_;
},
/**
* The DirectoryModel this tree corresponds to.
* @type {DirectoryModel}
*/
get directoryModel() {
return this.directoryModel_;
},
/**
* The VolumeManager instance of the system.
* @type {VolumeManager}
*/
get volumeManager() {
return this.volumeManager_;
},
/**
* The reference to shared MetadataModel instance.
* @type {!MetadataModel}
*/
get metadataModel() {
return this.metadataModel_;
},
set dataModel(dataModel) {
if (!this.onListContentChangedBound_) {
this.onListContentChangedBound_ = this.onListContentChanged_.bind(this);
}
if (this.dataModel_) {
this.dataModel_.removeEventListener(
'change', this.onListContentChangedBound_);
this.dataModel_.removeEventListener(
'permuted', this.onListContentChangedBound_);
}
this.dataModel_ = dataModel;
dataModel.addEventListener('change', this.onListContentChangedBound_);
dataModel.addEventListener('permuted', this.onListContentChangedBound_);
},
get dataModel() {
return this.dataModel_;
}
};
cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
cr.defineProperty(DirectoryTree, 'contextMenuForRootItems', cr.PropertyKind.JS);
/**
* Creates a new DirectoryItem based on |modelItem|.
* @param {NavigationModelItem} modelItem, model that will determine the type of
* DirectoryItem to be created.
* @param {!DirectoryTree} tree The tree to add the new DirectoryItem to.
* @return {!cr.ui.TreeItem} a newly created instance of a
* DirectoryItem type.
*/
DirectoryTree.createDirectoryItem = function(modelItem, tree) {
switch (modelItem.type) {
case NavigationModelItemType.VOLUME:
const volumeModelItem =
/** @type {NavigationModelVolumeItem} */ (modelItem);
if (volumeModelItem.volumeInfo.volumeType ===
VolumeManagerCommon.VolumeType.DRIVE) {
return new DriveVolumeItem(volumeModelItem, tree);
} else {
return new VolumeItem(volumeModelItem, tree);
}
break;
case NavigationModelItemType.SHORTCUT:
return new ShortcutItem(
/** @type {!NavigationModelShortcutItem} */ (modelItem), tree);
break;
case NavigationModelItemType.RECENT:
return new FakeItem(
VolumeManagerCommon.RootType.RECENT,
/** @type {!NavigationModelFakeItem} */ (modelItem), tree);
break;
case NavigationModelItemType.CROSTINI:
return new FakeItem(
VolumeManagerCommon.RootType.CROSTINI,
/** @type {!NavigationModelFakeItem} */ (modelItem), tree);
break;
case NavigationModelItemType.DRIVE:
return new FakeItem(
VolumeManagerCommon.RootType.DRIVE,
/** @type {!NavigationModelFakeItem} */ (modelItem), tree);
break;
case NavigationModelItemType.ENTRY_LIST:
const rootType = modelItem.section === NavigationSection.REMOVABLE ?
VolumeManagerCommon.RootType.REMOVABLE :
VolumeManagerCommon.RootType.MY_FILES;
return new EntryListItem(
rootType,
/** @type {!NavigationModelFakeItem} */ (modelItem), tree);
break;
}
assertNotReached(`No DirectoryItem model: "${modelItem.type}"`);
};
/**
* Updates and selects new directory.
* @param {!DirectoryEntry} parentDirectory Parent directory of new directory.
* @param {!DirectoryEntry} newDirectory
*/
DirectoryTree.prototype.updateAndSelectNewDirectory = function(
parentDirectory, newDirectory) {
// Expand parent directory.
const parentItem =
DirectoryItemTreeBaseMethods.getItemByEntry.call(this, parentDirectory);
parentItem.expanded = true;
// If new directory is already added to the tree, just select it.
for (let i = 0; i < parentItem.items.length; i++) {
const item = parentItem.items[i];
if (util.isSameEntry(item.entry, newDirectory)) {
this.selectedItem = item;
return;
}
}
// Create new item, and add it.
const newDirectoryItem =
new SubDirectoryItem(newDirectory.name, newDirectory, parentItem, this);
let addAt = 0;
while (addAt < parentItem.items.length &&
parentItem.items[addAt].entry.name < newDirectory.name) {
addAt++;
}
parentItem.addAt(newDirectoryItem, addAt);
this.selectedItem = newDirectoryItem;
};
/**
* Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
*
* @param {boolean} recursive True if the all visible sub-directories are
* updated recursively including left arrows. If false, the update walks
* only immediate child directories without arrows.
*/
DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
// First, current items which is not included in the dataModel should be
// removed.
for (let i = 0; i < this.items.length;) {
let found = false;
for (let j = 0; j < this.dataModel.length; j++) {
// Comparison by references, which is safe here, as model items are long
// living.
if (this.items[i].modelItem === this.dataModel.item(j)) {
found = true;
break;
}
}
if (!found) {
if (this.items[i].selected) {
this.items[i].selected = false;
}
this.remove(this.items[i]);
} else {
i++;
}
}
// Next, insert items which is in dataModel but not in current items.
let modelIndex = 0;
let itemIndex = 0;
// Initialize with first item's section so the first root doesn't get a
// divider line at the top.
let previousSection = this.dataModel.item(modelIndex).section;
while (modelIndex < this.dataModel.length) {
const currentItem = this.items[itemIndex];
if (itemIndex < this.items.length &&
currentItem.modelItem === this.dataModel.item(modelIndex)) {
const modelItem = currentItem.modelItem;
if (previousSection !== modelItem.section) {
currentItem.setAttribute('section-start', modelItem.section);
previousSection = modelItem.section;
}
if (recursive && currentItem instanceof VolumeItem) {
currentItem.updateSubDirectories(true);
}
// EntryListItem can contain volumes that might have been updated: ask
// them to re-draw. Updates recursively so any created or removed children
// folder can be reflected on directory tree.
if (currentItem instanceof EntryListItem) {
currentItem.updateSubDirectories(true);
}
} else {
const modelItem = this.dataModel.item(modelIndex);
if (modelItem) {
const item = DirectoryTree.createDirectoryItem(modelItem, this);
if (item) {
this.addAt(item, itemIndex);
if (previousSection !== modelItem.section) {
item.setAttribute('section-start', modelItem.section);
}
}
previousSection = modelItem.section;
}
}
itemIndex++;
modelIndex++;
}
if (itemIndex !== 0) {
this.hasChildren = true;
}
};
/**
* Finds a parent directory of the {@code entry} in {@code this}, and
* invokes the DirectoryItem.selectByEntry() of the found directory.
*
* @param {!DirectoryEntry|!FakeEntry} entry The entry to be searched for. Can
* be a fake.
* @return {boolean} True if the parent item is found.
*/
DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
// If the |entry| is same as one of volumes or shortcuts, select it.
for (let i = 0; i < this.items.length; i++) {
// Skips the Drive root volume. For Drive entries, one of children of Drive
// root or shortcuts should be selected.
const item = this.items[i];
if (item instanceof DriveVolumeItem) {
continue;
}
if (util.isSameEntry(item.entry, entry)) {
item.selectByEntry(entry);
return true;
}
}
// Otherwise, search whole tree.
const found =
DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
return found;
};
/**
* Decorates an element.
* @param {!DirectoryModel} directoryModel Current DirectoryModel.
* @param {!VolumeManager} volumeManager VolumeManager of the system.
* @param {!MetadataModel} metadataModel Shared MetadataModel instance.
* @param {!FileOperationManager} fileOperationManager
* @param {boolean} fakeEntriesVisible True if it should show the fakeEntries.
*/
DirectoryTree.prototype.decorateDirectoryTree = function(
directoryModel, volumeManager, metadataModel, fileOperationManager,
fakeEntriesVisible) {
cr.ui.Tree.prototype.decorate.call(this);
this.sequence_ = 0;
this.directoryModel_ = directoryModel;
this.volumeManager_ = volumeManager;
this.metadataModel_ = metadataModel;
this.models_ = [];
this.fileFilter_ = this.directoryModel_.getFileFilter();
this.fileFilter_.addEventListener('changed',
this.onFilterChanged_.bind(this));
this.directoryModel_.addEventListener('directory-changed',
this.onCurrentDirectoryChanged_.bind(this));
util.addEventListenerToBackgroundComponent(
fileOperationManager,
'entries-changed',
this.onEntriesChanged_.bind(this));
this.addEventListener('click', (event) => {
// Chromevox triggers |click| without switching focus, we force the focus
// here so we can handle further keyboard/mouse events to expand/collapse
// directories.
if (document.activeElement === document.body) {
this.focus();
}
});
this.privateOnDirectoryChangedBound_ =
this.onDirectoryContentChanged_.bind(this);
chrome.fileManagerPrivate.onDirectoryChanged.addListener(
this.privateOnDirectoryChangedBound_);
/**
* Flag to show fake entries in the tree.
* @type {boolean}
* @private
*/
this.fakeEntriesVisible_ = fakeEntriesVisible;
};
/**
* Handles entries changed event.
* @param {!Event} event
* @private
*/
DirectoryTree.prototype.onEntriesChanged_ = function(event) {
const directories = event.entries.filter((entry) => entry.isDirectory);
if (directories.length === 0) {
return;
}
switch (event.kind) {
case util.EntryChangedKind.CREATED:
// Handle as change event of parent entry.
Promise.all(
directories.map((directory) =>
new Promise(directory.getParent.bind(directory))))
.then((parentDirectories) => {
parentDirectories.forEach((parentDirectory) =>
this.updateTreeByEntry_(parentDirectory));
});
break;
case util.EntryChangedKind.DELETED:
directories.forEach((directory) => this.updateTreeByEntry_(directory));
break;
default:
assertNotReached();
}
};
/**
* Select the item corresponding to the given entry.
* @param {!DirectoryEntry|!FakeEntry} entry The directory entry to be selected.
* Can be a fake.
*/
DirectoryTree.prototype.selectByEntry = function(entry) {
if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry)) {
return;
}
if (this.searchAndSelectByEntry(entry)) {
return;
}
this.updateSubDirectories(false /* recursive */);
const currentSequence = ++this.sequence_;
const volumeInfo = this.volumeManager_.getVolumeInfo(entry);
if (!volumeInfo) {
return;
}
volumeInfo.resolveDisplayRoot(() => {
if (this.sequence_ !== currentSequence) {
return;
}
if (!this.searchAndSelectByEntry(entry)) {
this.selectedItem = null;
}
});
};
/**
* Activates the volume or the shortcut corresponding to the given index.
* @param {number} index 0-based index of the target top-level item.
* @return {boolean} True if one of the volume items is selected.
*/
DirectoryTree.prototype.activateByIndex = function(index) {
if (index < 0 || index >= this.items.length) {
return false;
}
this.items[index].selected = true;
this.items[index].activate();
return true;
};
/**
* Retrieves the latest subdirectories and update them on the tree.
*
* @param {boolean} recursive True if the update is recursively.
* @param {function()=} opt_callback Called when subdirectories are fully
* updated.
*/
DirectoryTree.prototype.updateSubDirectories = function(
recursive, opt_callback) {
this.redraw(recursive);
if (opt_callback) {
opt_callback();
}
};
/**
* Redraw the list.
* @param {boolean} recursive True if the update is recursively. False if the
* only root items are updated.
*/
DirectoryTree.prototype.redraw = function(recursive) {
this.updateSubElementsFromList(recursive);
};
/**
* Invoked when the filter is changed.
* @private
*/
DirectoryTree.prototype.onFilterChanged_ = function() {
// Returns immediately, if the tree is hidden.
if (this.hidden) {
return;
}
this.redraw(true /* recursive */);
};
/**
* Invoked when a directory is changed.
* @param {!chrome.fileManagerPrivate.FileWatchEvent} event Event.
* @private
*/
DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
if (event.eventType !== 'changed' || !event.entry) {
return;
}
this.updateTreeByEntry_(/** @type{!Entry} */ (event.entry));
};
/**
* Updates tree by entry.
* @param {!Entry} entry A changed entry. Deleted entry is passed when watched
* directory is deleted.
* @private
*/
DirectoryTree.prototype.updateTreeByEntry_ = function(entry) {
entry.getDirectory(entry.fullPath, {create: false},
() => {
// If entry exists.
// e.g. /a/b is deleted while watching /a.
for (let i = 0; i < this.items.length; i++) {
if (this.items[i] instanceof VolumeItem ||
this.items[i] instanceof EntryListItem) {
this.items[i].updateItemByEntry(entry);
}
}
},
() => {
// If entry does not exist, try to get parent and update the subtree by
// it.
// e.g. /a/b is deleted while watching /a/b. Try to update /a in this
// case.
entry.getParent((parentEntry) => {
this.updateTreeByEntry_(parentEntry);
}, (error) => {
// If it fails to get parent, update the subtree by volume.
// e.g. /a/b is deleted while watching /a/b/c. getParent of /a/b/c
// fails in this case. We falls back to volume update.
//
// TODO(yawano): Try to get parent path also in this case by
// manipulating path string.
const volumeInfo = this.volumeManager.getVolumeInfo(entry);
if (!volumeInfo) {
return;
}
for (let i = 0; i < this.items.length; i++) {
if (this.items[i] instanceof VolumeItem &&
this.items[i].volumeInfo === volumeInfo) {
this.items[i].updateSubDirectories(true /* recursive */);
}
}
});
});
};
/**
* Invoked when the current directory is changed.
* @param {!Event} event Event.
* @private
*/
DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
this.selectByEntry(event.newDirEntry);
this.updateSubDirectories(false /* recursive */, () => {});
};
/**
* Invoked when the volume list or shortcut list is changed.
* @private
*/
DirectoryTree.prototype.onListContentChanged_ = function() {
this.updateSubDirectories(false, () => {
// If no item is selected now, try to select the item corresponding to
// current directory because the current directory might have been populated
// in this tree in previous updateSubDirectories().
if (!this.selectedItem) {
const currentDir = this.directoryModel_.getCurrentDirEntry();
if (currentDir) {
this.selectByEntry(currentDir);
}
}
});
};
/**
* Updates the UI after the layout has changed.
*/
DirectoryTree.prototype.relayout = function() {
cr.dispatchSimpleEvent(this, 'relayout', true);
};