| // 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. |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 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. |
| */ |
| var DirectoryItemTreeBaseMethods = {}; |
| |
| /** |
| * 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 (var i = 0; i < this.items.length; i++) { |
| var item = this.items[i]; |
| if (!item.entry) |
| continue; |
| if (util.isDescendantEntry(item.entry, entry) || |
| util.isSameEntry(item.entry, entry)) { |
| item.selectByEntry(entry); |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| Object.freeze(DirectoryItemTreeBaseMethods); |
| |
| var TREE_ITEM_INNTER_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) { |
| var item = new cr.ui.TreeItem(); |
| item.__proto__ = DirectoryItem.prototype; |
| |
| item.parentTree_ = tree; |
| item.directoryModel_ = tree.directoryModel; |
| item.fileFilter_ = tree.directoryModel.getFileFilter(); |
| |
| item.innerHTML = TREE_ITEM_INNTER_HTML; |
| item.addEventListener('expand', item.onExpand_.bind(item), false); |
| |
| // Sets hasChildren=false tentatively. This will be overridden after |
| // scanning sub-directories in updateSubElementsFromList(). |
| item.hasChildren = false; |
| |
| item.label = label; |
| 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'); |
| } |
| }; |
| |
| /** |
| * 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) { |
| var index = 0; |
| var tree = this.parentTree_; |
| while (this.entries_[index]) { |
| var currentEntry = this.entries_[index]; |
| var currentElement = this.items[index]; |
| var label = util.getEntryLabel( |
| tree.volumeManager_.getLocationInfo(currentEntry), |
| currentEntry) || ''; |
| |
| if (index >= this.items.length) { |
| var item = new SubDirectoryItem(label, currentEntry, this, tree); |
| this.add(item); |
| index++; |
| } else if (util.isSameEntry(currentEntry, currentElement.entry)) { |
| currentElement.updateSharedStatusIcon(); |
| if (recursive && this.expanded) |
| currentElement.updateSubDirectories(true /* recursive */); |
| index++; |
| } else if (currentEntry.toURL() < currentElement.entry.toURL()) { |
| var item = new SubDirectoryItem(label, currentEntry, this, tree); |
| this.addAt(item, index); |
| index++; |
| } else if (currentEntry.toURL() > currentElement.entry.toURL()) { |
| this.remove(currentElement); |
| } |
| } |
| |
| var removedChild; |
| while (removedChild = this.items[index]) { |
| this.remove(removedChild); |
| } |
| |
| if (index === 0) { |
| this.hasChildren = false; |
| this.expanded = false; |
| } else { |
| this.hasChildren = true; |
| } |
| }; |
| |
| /** |
| * 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(child); |
| if (this.items.length == 0) |
| this.hasChildren = false; |
| }; |
| |
| /** |
| * Invoked when the item is being expanded. |
| * @param {!Event} e Event. |
| * @private |
| **/ |
| DirectoryItem.prototype.onExpand_ = function(e) { |
| this.updateSubDirectories( |
| true /* recursive */, |
| function() {}, |
| function() { |
| this.expanded = false; |
| }.bind(this)); |
| |
| 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 (!e.target.classList.contains('expand-icon')) |
| this.directoryModel_.activateDirectoryEntry(this.entry); |
| }; |
| |
| /** |
| * 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 || util.isFakeEntry(this.entry)) { |
| if (opt_errorCallback) |
| opt_errorCallback(); |
| return; |
| } |
| |
| var sortEntries = function(fileFilter, entries) { |
| entries.sort(util.compareName); |
| return entries.filter(fileFilter.filter.bind(fileFilter)); |
| }; |
| |
| var onSuccess = function(entries) { |
| this.entries_ = entries; |
| this.updateSubElementsFromList(recursive); |
| opt_successCallback && opt_successCallback(); |
| }.bind(this); |
| |
| var reader = this.entry.createReader(); |
| var entries = []; |
| var readEntry = function() { |
| reader.readEntries(function(results) { |
| if (!results.length) { |
| onSuccess(sortEntries(this.fileFilter_, entries)); |
| return; |
| } |
| |
| for (var i = 0; i < results.length; i++) { |
| var entry = results[i]; |
| if (entry.isDirectory) |
| entries.push(entry); |
| } |
| readEntry(); |
| }.bind(this)); |
| }.bind(this); |
| 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 (var i = 0; i < this.items.length; i++) { |
| var 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.updateSharedStatusIcon = 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() { |
| this.parentTree_.directoryModel.activateDirectoryEntry(this.entry); |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 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) { |
| var item = new DirectoryItem(label, tree); |
| item.__proto__ = SubDirectoryItem.prototype; |
| |
| item.dirEntry_ = dirEntry; |
| |
| // Sets up icons of the item. |
| var icon = item.querySelector('.icon'); |
| icon.classList.add('item-icon'); |
| var location = tree.volumeManager.getLocationInfo(item.entry); |
| if (location && location.rootType && location.isRootEntry) { |
| icon.setAttribute('volume-type-icon', location.rootType); |
| } else { |
| icon.setAttribute('file-type-icon', 'folder'); |
| item.updateSharedStatusIcon(); |
| } |
| |
| // Sets up context menu of the item. |
| if (tree.contextMenuForSubitems) |
| item.setContextMenu(tree.contextMenuForSubitems); |
| // Adds handler for future change. |
| tree.addEventListener( |
| 'contextMenuForSubitemsChange', |
| function(e) { item.setContextMenu(e.newValue); }); |
| |
| // Populates children now if needed. |
| if (parentDirItem.expanded) |
| item.updateSubDirectories(false /* recursive */); |
| |
| return item; |
| } |
| |
| SubDirectoryItem.prototype = { |
| __proto__: DirectoryItem.prototype, |
| |
| get entry() { |
| return this.dirEntry_; |
| } |
| }; |
| |
| /** |
| * Sets the context menu for directory tree. |
| * @param {!cr.ui.Menu} menu Menu to be set. |
| */ |
| SubDirectoryItem.prototype.setContextMenu = function(menu) { |
| var tree = this.parentTree_ || this; // If no parent, 'this' itself is tree. |
| var locationInfo = tree.volumeManager_.getLocationInfo(this.entry); |
| if (locationInfo && locationInfo.isEligibleForFolderShortcut) |
| cr.ui.contextMenuHandler.setContextMenu(this, menu); |
| }; |
| |
| /** |
| * Update the icon based on whether the folder is shared on Drive. |
| * @override |
| */ |
| SubDirectoryItem.prototype.updateSharedStatusIcon = function() { |
| var icon = this.querySelector('.icon'); |
| this.parentTree_.metadataModel.notifyEntriesChanged([this.dirEntry_]); |
| this.parentTree_.metadataModel.get([this.dirEntry_], ['shared']).then( |
| function(metadata) { |
| icon.classList.toggle('shared', metadata[0] && metadata[0].shared); |
| }); |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // VolumeItem |
| |
| /** |
| * A TreeItem which represents a volume. Volume items are displayed as |
| * top-level children of DirectoryTree. |
| * |
| * @param {NavigationModelItem} modelItem NavigationModelItem of this volume. |
| * @param {DirectoryTree} tree Current tree, which contains this item. |
| * @extends {DirectoryItem} |
| * @constructor |
| */ |
| function VolumeItem(modelItem, tree) { |
| var item = new DirectoryItem(modelItem.volumeInfo.label, tree); |
| item.__proto__ = VolumeItem.prototype; |
| |
| item.modelItem_ = modelItem; |
| item.volumeInfo_ = modelItem.volumeInfo; |
| |
| // Sets up icons of the item. |
| item.setupIcon_(item.querySelector('.icon'), item.volumeInfo_); |
| 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(function(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; |
| }, |
| get modelItem() { |
| return this.modelItem_; |
| } |
| }; |
| |
| /** |
| * Sets the context menu for volume items. |
| * @param {!cr.ui.Menu} menu Menu to be set. |
| */ |
| VolumeItem.prototype.setContextMenu = function(menu) { |
| if (this.isRemovable_()) |
| cr.ui.contextMenuHandler.setContextMenu(this, menu); |
| }; |
| |
| /** |
| * Change current entry to this volume's root directory. |
| * @override |
| */ |
| VolumeItem.prototype.activate = function() { |
| var directoryModel = this.parentTree_.directoryModel; |
| var onEntryResolved = function(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); |
| }.bind(this); |
| |
| this.volumeInfo_.resolveDisplayRoot( |
| onEntryResolved, |
| function() { |
| // Error, the display root is not available. It may happen on Drive. |
| this.parentTree_.dataModel.onItemNotFoundError(this.modelItem); |
| }.bind(this)); |
| }; |
| |
| /** |
| * @return {boolean} True if this volume can be removed by user operation. |
| * @private |
| */ |
| VolumeItem.prototype.isRemovable_ = function() { |
| var volumeType = this.volumeInfo_.volumeType; |
| return volumeType === VolumeManagerCommon.VolumeType.ARCHIVE || |
| volumeType === VolumeManagerCommon.VolumeType.REMOVABLE || |
| volumeType === VolumeManagerCommon.VolumeType.PROVIDED; |
| }; |
| |
| /** |
| * 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'); |
| if (volumeInfo.volumeType === VolumeManagerCommon.VolumeType.PROVIDED) { |
| var backgroundImage = '-webkit-image-set(' + |
| 'url(chrome://extension-icon/' + volumeInfo.extensionId + |
| '/16/1) 1x, ' + |
| 'url(chrome://extension-icon/' + volumeInfo.extensionId + |
| '/32/1) 2x);'; |
| // 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); |
| icon.setAttribute('volume-subtype', volumeInfo.deviceType || ''); |
| }; |
| |
| /** |
| * Set up eject button if needed. |
| * @param {HTMLElement} rowElement The parent element for eject button. |
| * @private |
| */ |
| VolumeItem.prototype.setupEjectButton_ = function(rowElement) { |
| if (this.isRemovable_()) { |
| var ejectButton = cr.doc.createElement('div'); |
| // Block other mouse handlers. |
| ejectButton.addEventListener( |
| 'mouseup', function(event) { event.stopPropagation() }); |
| ejectButton.addEventListener( |
| 'mousedown', function(event) { event.stopPropagation() }); |
| ejectButton.className = 'root-eject'; |
| ejectButton.setAttribute('aria-label', str('UNMOUNT_DEVICE_BUTTON_LABEL')); |
| ejectButton.addEventListener('click', function(event) { |
| event.stopPropagation(); |
| var 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); |
| }.bind(this)); |
| rowElement.appendChild(ejectButton); |
| |
| // Add paper-ripple effect on the eject button. |
| var ripple = cr.doc.createElement('paper-ripple'); |
| ripple.setAttribute('fit', ''); |
| ripple.className = 'circle recenteringTouch'; |
| ejectButton.appendChild(ripple); |
| } |
| }; |
| |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // DriveVolumeItem |
| |
| /** |
| * A TreeItem which represents a Drive volume. Drive volume has fake entries |
| * such as Recent, Shared with me, and Offline in it. |
| * |
| * @param {NavigationModelItem} modelItem NavigationModelItem of this volume. |
| * @param {DirectoryTree} tree Current tree, which contains this item. |
| * @extends {VolumeItem} |
| * @constructor |
| */ |
| function DriveVolumeItem(modelItem, tree) { |
| var item = new VolumeItem(modelItem, tree); |
| item.__proto__ = DriveVolumeItem.prototype; |
| item.classList.add('drive-volume'); |
| 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); |
| |
| if (!e.target.classList.contains('expand-icon')) { |
| // If the Drive volume is clicked, select one of the children instead of |
| // this item itself. |
| this.volumeInfo_.resolveDisplayRoot(function(displayRoot) { |
| this.searchAndSelectByEntry(displayRoot); |
| }.bind(this)); |
| } |
| }; |
| |
| /** |
| * 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) { |
| // Drive volume has children including fake entries (offline, recent, etc...). |
| if (this.entry && !this.hasChildren) { |
| var entries = [this.entry]; |
| if (this.parentTree_.fakeEntriesVisible_) { |
| for (var key in this.volumeInfo_.fakeEntries) |
| entries.push(this.volumeInfo_.fakeEntries[key]); |
| } |
| // This list is sorted by URL on purpose. |
| entries.sort(function(a, b) { |
| if (a.toURL() === b.toURL()) |
| return 0; |
| return b.toURL() > a.toURL() ? 1 : -1; |
| }); |
| |
| for (var i = 0; i < entries.length; i++) { |
| var item = new SubDirectoryItem( |
| util.getEntryLabel( |
| this.parentTree_.volumeManager_.getLocationInfo(entries[i]), |
| entries[i]) || '', |
| entries[i], this, this.parentTree_); |
| this.add(item); |
| item.updateSubDirectories(false); |
| } |
| this.expanded = true; |
| } |
| }; |
| |
| /** |
| * 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) { |
| 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 amang children. |
| this.searchAndSelectByEntry(entry); |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ShortcutItem |
| |
| /** |
| * A TreeItem which represents a shortcut for Drive folder. |
| * Shortcut items are displayed as top-level children of DirectoryTree. |
| * |
| * @param {NavigationModelItem} modelItem NavigationModelItem of this volume. |
| * @param {DirectoryTree} tree Current tree, which contains this item. |
| * @extends {cr.ui.TreeItem} |
| * @constructor |
| */ |
| function ShortcutItem(modelItem, tree) { |
| var item = new cr.ui.TreeItem(); |
| item.__proto__ = ShortcutItem.prototype; |
| |
| item.parentTree_ = tree; |
| item.dirEntry_ = modelItem.entry; |
| item.modelItem_ = modelItem; |
| |
| item.innerHTML = TREE_ITEM_INNTER_HTML; |
| |
| var icon = item.querySelector('.icon'); |
| icon.classList.add('item-icon'); |
| icon.setAttribute('volume-type-icon', VolumeManagerCommon.VolumeType.DRIVE); |
| |
| if (tree.contextMenuForRootItems) |
| item.setContextMenu(tree.contextMenuForRootItems); |
| |
| item.label = modelItem.entry.name; |
| 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); |
| this.activate(); |
| // Resets file selection when a volume is clicked. |
| this.parentTree_.directoryModel.clearSelection(); |
| }; |
| |
| /** |
| * 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. |
| */ |
| 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() { |
| var directoryModel = this.parentTree_.directoryModel; |
| var onEntryResolved = function(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); |
| } |
| }.bind(this); |
| |
| // 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, |
| function() { |
| // 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); |
| }.bind(this)); |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // CommandItem |
| |
| /** |
| * A TreeItem which represents a command button. |
| * Command items are displayed as top-level children of DirectoryTree. |
| * |
| * @param {NavigationModelCommandItem} modelItem |
| * @param {DirectoryTree} tree Current tree, which contains this item. |
| * @extends {cr.ui.TreeItem} |
| * @constructor |
| */ |
| function CommandItem(modelItem, tree) { |
| var item = new cr.ui.TreeItem(); |
| item.__proto__ = CommandItem.prototype; |
| |
| item.parentTree_ = tree; |
| item.modelItem_ = modelItem; |
| |
| item.innerHTML = TREE_ITEM_INNTER_HTML; |
| |
| var icon = item.querySelector('.icon'); |
| icon.classList.add('item-icon'); |
| icon.setAttribute('command-icon', modelItem.command.id); |
| |
| item.label = modelItem.label; |
| return item; |
| } |
| |
| CommandItem.prototype = { |
| __proto__: cr.ui.TreeItem.prototype, |
| get entry() { |
| return null; |
| }, |
| 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. |
| */ |
| CommandItem.prototype.searchAndSelectByEntry = function(entry) { |
| return false; |
| }; |
| |
| /** |
| * @override |
| */ |
| CommandItem.prototype.handleClick = function(e) { |
| this.activate(); |
| }; |
| |
| /** |
| * @param {!DirectoryEntry} entry |
| */ |
| CommandItem.prototype.selectByEntry = function(entry) { |
| }; |
| |
| /** |
| * Executes the command. |
| */ |
| CommandItem.prototype.activate = function() { |
| this.modelItem_.command.execute(); |
| }; |
| |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 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 {!VolumeManagerWrapper} volumeManager VolumeManager of the system. |
| * @param {!MetadataModel} metadataModel Shared MetadataModel instance. |
| * @param {boolean} fakeEntriesVisible True if it should show the fakeEntries. |
| */ |
| DirectoryTree.decorate = function( |
| el, directoryModel, volumeManager, metadataModel, fakeEntriesVisible) { |
| el.__proto__ = DirectoryTree.prototype; |
| /** @type {DirectoryTree} */ (el).decorateDirectoryTree( |
| directoryModel, volumeManager, metadataModel, 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); |
| |
| /** |
| * 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 (var i = 0; i < this.items.length;) { |
| var found = false; |
| for (var 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. |
| var modelIndex = 0; |
| var itemIndex = 0; |
| while (modelIndex < this.dataModel.length) { |
| if (itemIndex < this.items.length && |
| this.items[itemIndex].modelItem === this.dataModel.item(modelIndex)) { |
| if (recursive && this.items[itemIndex] instanceof VolumeItem) |
| this.items[itemIndex].updateSubDirectories(true); |
| } else { |
| var modelItem = this.dataModel.item(modelIndex); |
| switch (modelItem.type) { |
| case NavigationModelItem.Type.VOLUME: |
| if (modelItem.volumeInfo.volumeType === |
| VolumeManagerCommon.VolumeType.DRIVE) { |
| this.addAt(new DriveVolumeItem(modelItem, this), itemIndex); |
| } else { |
| this.addAt(new VolumeItem(modelItem, this), itemIndex); |
| } |
| break; |
| case NavigationModelItem.Type.SHORTCUT: |
| this.addAt(new ShortcutItem(modelItem, this), itemIndex); |
| break; |
| case NavigationModelItem.Type.COMMAND: |
| this.addAt(new CommandItem(modelItem, this), itemIndex); |
| break; |
| } |
| } |
| 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 (var 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. |
| var item = this.items[i]; |
| if (item instanceof DriveVolumeItem) |
| continue; |
| |
| if (util.isSameEntry(item.entry, entry)) { |
| item.selectByEntry(entry); |
| return true; |
| } |
| } |
| // Otherwise, search whole tree. |
| var found = DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call( |
| this, entry); |
| return found; |
| }; |
| |
| /** |
| * Decorates an element. |
| * @param {!DirectoryModel} directoryModel Current DirectoryModel. |
| * @param {!VolumeManagerWrapper} volumeManager VolumeManager of the system. |
| * @param {!MetadataModel} metadataModel Shared MetadataModel instance. |
| * @param {boolean} fakeEntriesVisible True if it should show the fakeEntries. |
| */ |
| DirectoryTree.prototype.decorateDirectoryTree = function( |
| directoryModel, volumeManager, metadataModel, 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)); |
| |
| this.privateOnDirectoryChangedBound_ = |
| this.onDirectoryContentChanged_.bind(this); |
| chrome.fileManagerPrivate.onDirectoryChanged.addListener( |
| this.privateOnDirectoryChangedBound_); |
| |
| this.scrollBar_ = new ScrollBar(); |
| this.scrollBar_.initialize(this.parentElement, this); |
| |
| /** |
| * Flag to show fake entries in the tree. |
| * @type {boolean} |
| * @private |
| */ |
| this.fakeEntriesVisible_ = fakeEntriesVisible; |
| }; |
| |
| /** |
| * 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 */); |
| var currentSequence = ++this.sequence_; |
| var volumeInfo = this.volumeManager_.getVolumeInfo(entry); |
| if (!volumeInfo) |
| return; |
| volumeInfo.resolveDisplayRoot(function() { |
| if (this.sequence_ !== currentSequence) |
| return; |
| if (!this.searchAndSelectByEntry(entry)) |
| this.selectedItem = null; |
| }.bind(this)); |
| }; |
| |
| /** |
| * 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); |
| }; |
| |
| /** |
| * Handles keydown events on the tree and activates the selected item on Enter. |
| * @param {Event} e The click event object. |
| * @override |
| */ |
| DirectoryTree.prototype.handleKeyDown = function(e) { |
| cr.ui.Tree.prototype.handleKeyDown.call(this, e); |
| if (util.getKeyModifiers(e) === '' && e.keyIdentifier === 'Enter') { |
| if (this.selectedItem) |
| this.selectedItem.activate(); |
| } |
| }; |
| |
| /** |
| * 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 {!Event} event Event. |
| * @private |
| */ |
| DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) { |
| if (event.eventType !== 'changed' || !event.entry) |
| return; |
| |
| for (var i = 0; i < this.items.length; i++) { |
| if (this.items[i] instanceof VolumeItem) |
| this.items[i].updateItemByEntry(event.entry); |
| } |
| }; |
| |
| /** |
| * Invoked when the current directory is changed. |
| * @param {!Event} event Event. |
| * @private |
| */ |
| DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) { |
| this.selectByEntry(event.newDirEntry); |
| }; |
| |
| /** |
| * Invoked when the volume list or shortcut list is changed. |
| * @private |
| */ |
| DirectoryTree.prototype.onListContentChanged_ = function() { |
| this.updateSubDirectories(false, function() { |
| // 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) { |
| var currentDir = this.directoryModel_.getCurrentDirEntry(); |
| if (currentDir) |
| this.selectByEntry(currentDir); |
| } |
| }.bind(this)); |
| }; |
| |
| /** |
| * Updates the UI after the layout has changed. |
| */ |
| DirectoryTree.prototype.relayout = function() { |
| cr.dispatchSimpleEvent(this, 'relayout', true); |
| }; |