1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 ////////////////////////////////////////////////////////////////////////////////
11 * Implementation of methods for DirectoryTree and DirectoryItem. These classes
12 * inherits cr.ui.Tree/TreeItem so we can't make them inherit this class.
13 * Instead, we separate their implementations to this separate object and call
14 * it with setting 'this' from DirectoryTree/Item.
16 var DirectoryItemTreeBaseMethods = {};
19 * Updates sub-elements of {@code this} reading {@code DirectoryEntry}.
20 * The list of {@code DirectoryEntry} are not updated by this method.
22 * @param {boolean} recursive True if the all visible sub-directories are
23 * updated recursively including left arrows. If false, the update walks
24 * only immediate child directories without arrows.
26 DirectoryItemTreeBaseMethods.updateSubElementsFromList = function(recursive) {
28 var tree = this.parentTree_ || this; // If no parent, 'this' itself is tree.
29 while (this.entries_[index]) {
30 var currentEntry = this.entries_[index];
31 var currentElement = this.items[index];
32 var label = util.getEntryLabel(tree.volumeManager_, currentEntry);
34 if (index >= this.items.length) {
35 var item = new DirectoryItem(label, currentEntry, this, tree);
38 } else if (util.isSameEntry(currentEntry, currentElement.entry)) {
39 currentElement.updateSharedStatusIcon();
40 if (recursive && this.expanded)
41 currentElement.updateSubDirectories(true /* recursive */);
44 } else if (currentEntry.toURL() < currentElement.entry.toURL()) {
45 var item = new DirectoryItem(label, currentEntry, this, tree);
46 this.addAt(item, index);
48 } else if (currentEntry.toURL() > currentElement.entry.toURL()) {
49 this.remove(currentElement);
54 while (removedChild = this.items[index]) {
55 this.remove(removedChild);
59 this.hasChildren = false;
60 this.expanded = false;
62 this.hasChildren = true;
67 * Finds a parent directory of the {@code entry} in {@code this}, and
68 * invokes the DirectoryItem.selectByEntry() of the found directory.
70 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
72 * @return {boolean} True if the parent item is found.
74 DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
75 for (var i = 0; i < this.items.length; i++) {
76 var item = this.items[i];
77 if (util.isDescendantEntry(item.entry, entry) ||
78 util.isSameEntry(item.entry, entry)) {
79 item.selectByEntry(entry);
86 Object.freeze(DirectoryItemTreeBaseMethods);
88 var TREE_ITEM_INNTER_HTML =
89 '<div class="tree-row">' +
90 ' <span class="expand-icon"></span>' +
91 ' <span class="icon"></span>' +
92 ' <span class="label entry-name"></span>' +
94 '<div class="tree-children"></div>';
96 ////////////////////////////////////////////////////////////////////////////////
100 * A directory in the tree. Each element represents one directory.
102 * @param {string} label Label for this item.
103 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
104 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
105 * @param {DirectoryTree} tree Current tree, which contains this item.
106 * @extends {cr.ui.TreeItem}
109 function DirectoryItem(label, dirEntry, parentDirItem, tree) {
110 var item = new cr.ui.TreeItem();
111 DirectoryItem.decorate(item, label, dirEntry, parentDirItem, tree);
116 * @param {HTMLElement} el Element to be DirectoryItem.
117 * @param {string} label Label for this item.
118 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
119 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
120 * @param {DirectoryTree} tree Current tree, which contains this item.
122 DirectoryItem.decorate =
123 function(el, label, dirEntry, parentDirItem, tree) {
124 el.__proto__ = DirectoryItem.prototype;
125 (/** @type {DirectoryItem} */ el).decorate(
126 label, dirEntry, parentDirItem, tree);
129 DirectoryItem.prototype = {
130 __proto__: cr.ui.TreeItem.prototype,
133 * The DirectoryEntry corresponding to this DirectoryItem. This may be
134 * a dummy DirectoryEntry.
135 * @type {DirectoryEntry|Object}
138 return this.dirEntry_;
142 * The element containing the label text and the icon.
143 * @type {!HTMLElement}
147 return this.firstElementChild.querySelector('.label');
152 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
154 * @param {boolean} recursive True if the all visible sub-directories are
155 * updated recursively including left arrows. If false, the update walks
156 * only immediate child directories without arrows.
158 DirectoryItem.prototype.updateSubElementsFromList = function(recursive) {
159 DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
163 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
165 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
167 * @return {boolean} True if the parent item is found.
169 DirectoryItem.prototype.searchAndSelectByEntry = function(entry) {
170 return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
174 * @param {string} label Localized label for this item.
175 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
176 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
177 * @param {DirectoryTree} tree Current tree, which contains this item.
179 DirectoryItem.prototype.decorate = function(
180 label, dirEntry, parentDirItem, tree) {
181 this.innerHTML = TREE_ITEM_INNTER_HTML;
182 this.parentTree_ = tree;
183 this.directoryModel_ = tree.directoryModel;
184 this.parent_ = parentDirItem;
186 this.dirEntry_ = dirEntry;
187 this.fileFilter_ = this.directoryModel_.getFileFilter();
189 // Sets hasChildren=false tentatively. This will be overridden after
190 // scanning sub-directories in updateSubElementsFromList().
191 this.hasChildren = false;
193 this.addEventListener('expand', this.onExpand_.bind(this), false);
194 var icon = this.querySelector('.icon');
195 icon.classList.add('volume-icon');
196 var location = tree.volumeManager.getLocationInfo(dirEntry);
197 if (location && location.rootType && location.isRootEntry) {
198 icon.setAttribute('volume-type-icon', location.rootType);
200 icon.setAttribute('file-type-icon', 'folder');
201 this.updateSharedStatusIcon();
204 if (this.parentTree_.contextMenuForSubitems)
205 this.setContextMenu(this.parentTree_.contextMenuForSubitems);
206 // Adds handler for future change.
207 this.parentTree_.addEventListener(
208 'contextMenuForSubitemsChange',
209 function(e) { this.setContextMenu(e.newValue); }.bind(this));
211 if (parentDirItem.expanded)
212 this.updateSubDirectories(false /* recursive */);
216 * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
217 * a complex layout. This call is not necessary, so we are ignoring it.
219 * @param {boolean} unused Unused.
222 DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
226 * Removes the child node, but without selecting the parent item, to avoid
227 * unintended changing of directories. Removing is done externally, and other
228 * code will navigate to another directory.
230 * @param {!cr.ui.TreeItem} child The tree item child to remove.
233 DirectoryItem.prototype.remove = function(child) {
234 this.lastElementChild.removeChild(child);
235 if (this.items.length == 0)
236 this.hasChildren = false;
240 * Invoked when the item is being expanded.
241 * @param {!UIEvent} e Event.
244 DirectoryItem.prototype.onExpand_ = function(e) {
245 this.updateSubDirectories(
246 true /* recursive */,
249 this.expanded = false;
256 * Invoked when the tree item is clicked.
258 * @param {Event} e Click event.
261 DirectoryItem.prototype.handleClick = function(e) {
262 cr.ui.TreeItem.prototype.handleClick.call(this, e);
263 if (!e.target.classList.contains('expand-icon'))
264 this.directoryModel_.activateDirectoryEntry(this.entry);
268 * Retrieves the latest subdirectories and update them on the tree.
269 * @param {boolean} recursive True if the update is recursively.
270 * @param {function()=} opt_successCallback Callback called on success.
271 * @param {function()=} opt_errorCallback Callback called on error.
273 DirectoryItem.prototype.updateSubDirectories = function(
274 recursive, opt_successCallback, opt_errorCallback) {
275 if (util.isFakeEntry(this.entry)) {
276 if (opt_errorCallback)
281 var sortEntries = function(fileFilter, entries) {
282 entries.sort(util.compareName);
283 return entries.filter(fileFilter.filter.bind(fileFilter));
286 var onSuccess = function(entries) {
287 this.entries_ = entries;
288 this.redrawSubDirectoryList_(recursive);
289 opt_successCallback && opt_successCallback();
292 var reader = this.entry.createReader();
294 var readEntry = function() {
295 reader.readEntries(function(results) {
296 if (!results.length) {
297 onSuccess(sortEntries(this.fileFilter_, entries));
301 for (var i = 0; i < results.length; i++) {
302 var entry = results[i];
303 if (entry.isDirectory)
313 * Searches for the changed directory in the current subtree, and if it is found
316 * @param {DirectoryEntry} changedDirectoryEntry The entry ot the changed
319 DirectoryItem.prototype.updateItemByEntry = function(changedDirectoryEntry) {
320 if (util.isSameEntry(changedDirectoryEntry, this.entry)) {
321 this.updateSubDirectories(false /* recursive */);
325 // Traverse the entire subtree to find the changed element.
326 for (var i = 0; i < this.items.length; i++) {
327 var item = this.items[i];
328 if (util.isDescendantEntry(item.entry, changedDirectoryEntry) ||
329 util.isSameEntry(item.entry, changedDirectoryEntry)) {
330 item.updateItemByEntry(changedDirectoryEntry);
337 * Update the icon based on whether the folder is shared on Drive.
339 DirectoryItem.prototype.updateSharedStatusIcon = function() {
340 var icon = this.querySelector('.icon');
341 this.parentTree_.metadataCache.getLatest(
345 icon.classList.toggle('shared', metadata[0] && metadata[0].shared);
350 * Redraw subitems with the latest information. The items are sorted in
351 * alphabetical order, case insensitive.
352 * @param {boolean} recursive True if the update is recursively.
355 DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
356 this.updateSubElementsFromList(recursive);
360 * Select the item corresponding to the given {@code entry}.
361 * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
363 DirectoryItem.prototype.selectByEntry = function(entry) {
364 if (util.isSameEntry(entry, this.entry)) {
365 this.selected = true;
369 if (this.searchAndSelectByEntry(entry))
372 // If the entry doesn't exist, updates sub directories and tries again.
373 this.updateSubDirectories(
374 false /* recursive */,
375 this.searchAndSelectByEntry.bind(this, entry));
379 * Executes the assigned action as a drop target.
381 DirectoryItem.prototype.doDropTargetAction = function() {
382 this.expanded = true;
386 * Sets the context menu for directory tree.
387 * @param {cr.ui.Menu} menu Menu to be set.
389 DirectoryItem.prototype.setContextMenu = function(menu) {
390 var tree = this.parentTree_ || this; // If no parent, 'this' itself is tree.
391 var locationInfo = tree.volumeManager_.getLocationInfo(this.entry);
392 if (locationInfo && locationInfo.isEligibleForFolderShortcut)
393 cr.ui.contextMenuHandler.setContextMenu(this, menu);
397 * Change current directory to the entry of this item.
399 DirectoryItem.prototype.activate = function() {
400 this.parentTree_.directoryModel.activateDirectoryEntry(this.entry);
403 ////////////////////////////////////////////////////////////////////////////////
407 * A TreeItem which represents a volume. Volume items are displayed as
408 * top-level children of DirectoryTree.
410 * @param {NavigationModelItem} modelItem NavigationModelItem of this volume.
411 * @param {DirectoryTree} tree Current tree, which contains this item.
412 * @extends {cr.ui.TreeItem}
415 function VolumeItem(modelItem, tree) {
416 var item = new cr.ui.TreeItem();
417 item.__proto__ = VolumeItem.prototype;
418 item.decorate(modelItem, tree);
422 VolumeItem.prototype = {
423 __proto__: cr.ui.TreeItem.prototype,
425 return this.volumeInfo_.displayRoot;
428 return this.modelItem_;
431 return this.volumeInfo_;
434 return this.firstElementChild.querySelector('.label');
436 // Overrides the property 'expanded' to prevent volume items from shrinking.
438 return Object.getOwnPropertyDescriptor(
439 cr.ui.TreeItem.prototype, 'expanded').get.call(this);
444 Object.getOwnPropertyDescriptor(
445 cr.ui.TreeItem.prototype, 'expanded').set.call(this, b);
450 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
452 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
454 * @return {boolean} True if the parent item is found.
456 VolumeItem.prototype.searchAndSelectByEntry = function(entry) {
457 return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
461 * Decorates this element.
462 * @param {NavigationModelItem} modelItem NavigationModelItem of this volume.
463 * @param {DirectoryTree} tree Current tree, which contains this item.
465 VolumeItem.prototype.decorate = function(modelItem, tree) {
466 this.innerHTML = TREE_ITEM_INNTER_HTML;
467 this.parentTree_ = tree;
468 this.modelItem_ = modelItem;
469 this.volumeInfo_ = modelItem.volumeInfo;
470 this.label = modelItem.volumeInfo.label;
472 this.setupIcon_(this.querySelector('.icon'), this.volumeInfo);
473 this.setupEjectButton_(this.rowElement);
474 if (tree.contextMenuForRootItems)
475 this.setContextMenu(tree.contextMenuForRootItems);
477 // Populate children of this volume using resolved display root.
478 this.volumeInfo_.resolveDisplayRoot(function(displayRoot) {
479 this.updateSubDirectories(false /* recursive */);
484 * Invoked when the tree item is clicked.
486 * @param {Event} e Click event.
489 VolumeItem.prototype.handleClick = function(e) {
490 // If the currently selected volume is clicked, change current directory to
491 // the volume's root.
495 cr.ui.TreeItem.prototype.handleClick.call(this, e);
497 // Resets file selection when a volume is clicked.
498 this.parentTree_.directoryModel.clearSelection();
500 // If the Drive volume is clicked, select one of the children instead of this
502 if (this.isDrive()) {
503 this.volumeInfo_.resolveDisplayRoot(function(displayRoot) {
504 this.searchAndSelectByEntry(displayRoot);
510 * Retrieves the latest subdirectories and update them on the tree.
511 * @param {boolean} recursive True if the update is recursively.
513 VolumeItem.prototype.updateSubDirectories = function(recursive) {
514 // Drive volume has children including fake entries (offline, recent, etc...).
515 if (this.isDrive() && this.entry && !this.hasChildren) {
516 var entries = [this.entry];
517 if (this.parentTree_.fakeEntriesVisible_) {
518 for (var key in this.volumeInfo.fakeEntries)
519 entries.push(this.volumeInfo.fakeEntries[key]);
521 // This list is sorted by URL on purpose.
522 entries.sort(function(a, b) { return a.toURL() < b.toURL(); });
524 for (var i = 0; i < entries.length; i++) {
525 var item = new DirectoryItem(
526 util.getEntryLabel(this.parentTree_.volumeManager_, entries[i]),
527 entries[i], this, this.parentTree_);
529 item.updateSubDirectories(false);
531 this.expanded = true;
536 * Searches for the changed directory in the current subtree, and if it is found
539 * @param {DirectoryEntry} changedDirectoryEntry The entry ot the changed
542 VolumeItem.prototype.updateItemByEntry = function(changedDirectoryEntry) {
544 this.items[0].updateItemByEntry(changedDirectoryEntry);
548 * Select the item corresponding to the given entry.
549 * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
552 VolumeItem.prototype.selectByEntry = function(entry) {
553 // If this volume is drive, find the item to be selected amang children.
554 if (this.isDrive()) {
555 this.searchAndSelectByEntry(entry);
558 if (util.isSameEntry(this.entry, entry) ||
559 util.isDescendantEntry(this.entry, entry))
560 this.selected = true;
564 * Sets the context menu for volume items.
565 * @param {cr.ui.Menu} menu Menu to be set.
567 VolumeItem.prototype.setContextMenu = function(menu) {
568 if (this.isRemovable_())
569 cr.ui.contextMenuHandler.setContextMenu(this, menu);
573 * Change current entry to this volume's root directory.
575 VolumeItem.prototype.activate = function() {
576 var directoryModel = this.parentTree_.directoryModel;
577 var onEntryResolved = function(entry) {
578 // Changes directory to the model item's root directory if needed.
579 if (!util.isSameEntry(directoryModel.getCurrentDirEntry(), entry)) {
580 metrics.recordUserAction('FolderShortcut.Navigate');
581 directoryModel.changeDirectoryEntry(entry);
583 // In case of failure in resolveDisplayRoot() in the volume's decorate(),
584 // update the volume's children here.
585 this.updateSubDirectories(false);
588 this.volumeInfo.resolveDisplayRoot(
591 // Error, the display root is not available. It may happen on Drive.
592 this.parentTree_.dataModel.onItemNotFoundError(this.modelItem);
597 * @return {boolean} True if this is Drive volume.
599 VolumeItem.prototype.isDrive = function() {
600 return this.volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DRIVE;
604 * @return {boolean} True if this volume can be removed by user operation.
607 VolumeItem.prototype.isRemovable_ = function() {
608 var volumeType = this.volumeInfo.volumeType;
609 return volumeType === VolumeManagerCommon.VolumeType.ARCHIVE ||
610 volumeType === VolumeManagerCommon.VolumeType.REMOVABLE ||
611 volumeType === VolumeManagerCommon.VolumeType.PROVIDED;
615 * Set up icon of this volume item.
616 * @param {HTMLElement} icon Icon element to be setup.
617 * @param {VolumeInfo} volumeInfo VolumeInfo determines the icon type.
620 VolumeItem.prototype.setupIcon_ = function(icon, volumeInfo) {
621 icon.classList.add('volume-icon');
622 if (volumeInfo.volumeType === 'provided') {
623 var backgroundImage = '-webkit-image-set(' +
624 'url(chrome://extension-icon/' + volumeInfo.extensionId +
626 'url(chrome://extension-icon/' + volumeInfo.extensionId +
628 // The icon div is not yet added to DOM, therefore it is impossible to
629 // use style.backgroundImage.
631 'style', 'background-image: ' + backgroundImage);
633 icon.setAttribute('volume-type-icon', volumeInfo.volumeType);
634 icon.setAttribute('volume-subtype', volumeInfo.deviceType);
638 * Set up eject button if needed.
639 * @param {HTMLElement} rowElement The parent element for eject button.
642 VolumeItem.prototype.setupEjectButton_ = function(rowElement) {
643 if (this.isRemovable_()) {
644 var ejectButton = cr.doc.createElement('div');
645 // Block other mouse handlers.
646 ejectButton.addEventListener(
647 'mouseup', function(event) { event.stopPropagation() });
648 ejectButton.addEventListener(
649 'mousedown', function(event) { event.stopPropagation() });
650 ejectButton.className = 'root-eject';
651 ejectButton.addEventListener('click', function(event) {
652 event.stopPropagation();
653 var unmountCommand = cr.doc.querySelector('command#unmount');
654 // Let's make sure 'canExecute' state of the command is properly set for
655 // the root before executing it.
656 unmountCommand.canExecuteChange(this);
657 unmountCommand.execute(this);
659 rowElement.appendChild(ejectButton);
663 ////////////////////////////////////////////////////////////////////////////////
667 * A TreeItem which represents a shortcut for Drive folder.
668 * Shortcut items are displayed as top-level children of DirectoryTree.
670 * @param {NavigationModelItem} modelItem NavigationModelItem of this volume.
671 * @param {DirectoryTree} tree Current tree, which contains this item.
672 * @extends {cr.ui.TreeItem}
675 function ShortcutItem(modelItem, tree) {
676 var item = new cr.ui.TreeItem();
677 item.__proto__ = ShortcutItem.prototype;
678 item.decorate(modelItem, tree);
682 ShortcutItem.prototype = {
683 __proto__: cr.ui.TreeItem.prototype,
685 return this.dirEntry_;
688 return this.modelItem_;
691 return this.firstElementChild.querySelector('.label');
696 * Finds a parent directory of the {@code entry} in {@code this}, and
697 * invokes the DirectoryItem.selectByEntry() of the found directory.
699 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
701 * @return {boolean} True if the parent item is found.
703 ShortcutItem.prototype.searchAndSelectByEntry = function(entry) {
704 // Always false as shortcuts have no children.
709 * Decorates this element.
710 * @param {NavigationModelItem} modelItem NavigationModelItem of this volume.
711 * @param {DirectoryTree} tree Current tree, which contains this item.
713 ShortcutItem.prototype.decorate = function(modelItem, tree) {
714 this.innerHTML = TREE_ITEM_INNTER_HTML;
715 this.parentTree_ = tree;
716 this.label = modelItem.entry.name;
717 this.dirEntry_ = modelItem.entry;
718 this.modelItem_ = modelItem;
720 var icon = this.querySelector('.icon');
721 icon.classList.add('volume-icon');
722 icon.setAttribute('volume-type-icon', VolumeManagerCommon.VolumeType.DRIVE);
724 if (tree.contextMenuForRootItems)
725 this.setContextMenu(tree.contextMenuForRootItems);
729 * Invoked when the tree item is clicked.
731 * @param {Event} e Click event.
734 ShortcutItem.prototype.handleClick = function(e) {
735 cr.ui.TreeItem.prototype.handleClick.call(this, e);
736 this.parentTree_.directoryModel.clearSelection();
740 * Select the item corresponding to the given entry.
741 * @param {DirectoryEntry} entry The directory entry to be selected.
743 ShortcutItem.prototype.selectByEntry = function(entry) {
744 if (util.isSameEntry(entry, this.entry))
745 this.selected = true;
749 * Sets the context menu for shortcut items.
750 * @param {cr.ui.Menu} menu Menu to be set.
752 ShortcutItem.prototype.setContextMenu = function(menu) {
753 cr.ui.contextMenuHandler.setContextMenu(this, menu);
757 * Change current entry to the entry corresponding to this shortcut.
759 ShortcutItem.prototype.activate = function() {
760 var directoryModel = this.parentTree_.directoryModel;
761 var onEntryResolved = function(entry) {
762 // Changes directory to the model item's root directory if needed.
763 if (!util.isSameEntry(directoryModel.getCurrentDirEntry(), entry)) {
764 metrics.recordUserAction('FolderShortcut.Navigate');
765 directoryModel.changeDirectoryEntry(entry);
769 // For shortcuts we already have an Entry, but it has to be resolved again
770 // in case, it points to a non-existing directory.
771 webkitResolveLocalFileSystemURL(
775 // Error, the entry can't be re-resolved. It may happen for shortcuts
776 // which targets got removed after resolving the Entry during
778 this.parentTree_.dataModel.onItemNotFoundError(this.modelItem);
782 ////////////////////////////////////////////////////////////////////////////////
786 * Tree of directories on the middle bar. This element is also the root of
787 * items, in other words, this is the parent of the top-level items.
790 * @extends {cr.ui.Tree}
792 function DirectoryTree() {}
795 * Decorates an element.
796 * @param {HTMLElement} el Element to be DirectoryTree.
797 * @param {DirectoryModel} directoryModel Current DirectoryModel.
798 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
799 * @param {MetadataCache} metadataCache Shared MetadataCache instance.
800 * @param {boolean} fakeEntriesVisible True if it should show the fakeEntries.
802 DirectoryTree.decorate = function(
803 el, directoryModel, volumeManager, metadataCache, fakeEntriesVisible) {
804 el.__proto__ = DirectoryTree.prototype;
805 (/** @type {DirectoryTree} */ el).decorate(
806 directoryModel, volumeManager, metadataCache, fakeEntriesVisible);
809 DirectoryTree.prototype = {
810 __proto__: cr.ui.Tree.prototype,
812 // DirectoryTree is always expanded.
813 get expanded() { return true; },
815 * @param {boolean} value Not used.
817 set expanded(value) {},
820 * The DirectoryEntry corresponding to this DirectoryItem. This may be
821 * a dummy DirectoryEntry.
822 * @type {DirectoryEntry|Object}
826 return this.dirEntry_;
830 * The DirectoryModel this tree corresponds to.
831 * @type {DirectoryModel}
833 get directoryModel() {
834 return this.directoryModel_;
838 * The VolumeManager instance of the system.
839 * @type {VolumeManager}
841 get volumeManager() {
842 return this.volumeManager_;
846 * The reference to shared MetadataCache instance.
847 * @type {MetadataCache}
849 get metadataCache() {
850 return this.metadataCache_;
853 set dataModel(dataModel) {
854 if (!this.onListContentChangedBound_)
855 this.onListContentChangedBound_ = this.onListContentChanged_.bind(this);
857 if (this.dataModel_) {
858 this.dataModel_.removeEventListener(
859 'change', this.onListContentChangedBound_);
860 this.dataModel_.removeEventListener(
861 'permuted', this.onListContentChangedBound_);
863 this.dataModel_ = dataModel;
864 dataModel.addEventListener('change', this.onListContentChangedBound_);
865 dataModel.addEventListener('permuted', this.onListContentChangedBound_);
869 return this.dataModel_;
873 cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
874 cr.defineProperty(DirectoryTree, 'contextMenuForRootItems', cr.PropertyKind.JS);
877 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
879 * @param {boolean} recursive True if the all visible sub-directories are
880 * updated recursively including left arrows. If false, the update walks
881 * only immediate child directories without arrows.
883 DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
884 // First, current items which is not included in the dataModel should be
886 for (var i = 0; i < this.items.length;) {
888 for (var j = 0; j < this.dataModel.length; j++) {
889 if (NavigationModelItem.isSame(this.items[i].modelItem,
890 this.dataModel.item(j))) {
896 if (this.items[i].selected)
897 this.items[i].selected = false;
898 this.remove(this.items[i]);
904 // Next, insert items which is in dataModel but not in current items.
907 while (modelIndex < this.dataModel.length) {
908 if (itemIndex < this.items.length &&
909 NavigationModelItem.isSame(this.items[itemIndex].modelItem,
910 this.dataModel.item(modelIndex))) {
911 if (recursive && this.items[itemIndex] instanceof VolumeItem)
912 this.items[itemIndex].updateSubDirectories(true);
914 var modelItem = this.dataModel.item(modelIndex);
915 if (modelItem.isVolume)
916 this.addAt(new VolumeItem(modelItem, this), itemIndex);
918 this.addAt(new ShortcutItem(modelItem, this), itemIndex);
925 this.hasChildren = true;
929 * Finds a parent directory of the {@code entry} in {@code this}, and
930 * invokes the DirectoryItem.selectByEntry() of the found directory.
932 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
934 * @return {boolean} True if the parent item is found.
936 DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
937 // If the |entry| is same as one of volumes or shortcuts, select it.
938 for (var i = 0; i < this.items.length; i++) {
939 // Skips the Drive root volume. For Drive entries, one of children of Drive
940 // root or shortcuts should be selected.
941 var item = this.items[i];
942 if (item instanceof VolumeItem && item.isDrive())
945 if (util.isSameEntry(item.entry, entry)) {
946 this.dontHandleChangeEvent_ = true;
947 item.selectByEntry(entry);
948 this.dontHandleChangeEvent_ = false;
952 // Otherwise, search whole tree.
953 this.dontHandleChangeEvent_ = true;
954 var found = DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(
956 this.dontHandleChangeEvent_ = false;
961 * Decorates an element.
962 * @param {DirectoryModel} directoryModel Current DirectoryModel.
963 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
964 * @param {MetadataCache} metadataCache Shared MetadataCache instance.
965 * @param {boolean} fakeEntriesVisible True if it should show the fakeEntries.
967 DirectoryTree.prototype.decorate = function(
968 directoryModel, volumeManager, metadataCache, fakeEntriesVisible) {
969 cr.ui.Tree.prototype.decorate.call(this);
972 this.directoryModel_ = directoryModel;
973 this.volumeManager_ = volumeManager;
974 this.metadataCache_ = metadataCache;
977 this.fileFilter_ = this.directoryModel_.getFileFilter();
978 this.fileFilter_.addEventListener('changed',
979 this.onFilterChanged_.bind(this));
981 this.directoryModel_.addEventListener('directory-changed',
982 this.onCurrentDirectoryChanged_.bind(this));
984 // Add a handler for directory change.
985 this.addEventListener('change', function() {
986 if (this.selectedItem && !this.dontHandleChangeEvent_)
987 this.selectedItem.activate();
990 this.privateOnDirectoryChangedBound_ =
991 this.onDirectoryContentChanged_.bind(this);
992 chrome.fileManagerPrivate.onDirectoryChanged.addListener(
993 this.privateOnDirectoryChangedBound_);
995 this.scrollBar_ = new MainPanelScrollBar();
996 this.scrollBar_.initialize(this.parentNode, this);
999 * Flag to show fake entries in the tree.
1003 this.fakeEntriesVisible_ = fakeEntriesVisible;
1007 * Select the item corresponding to the given entry.
1008 * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
1011 DirectoryTree.prototype.selectByEntry = function(entry) {
1012 if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
1015 if (this.searchAndSelectByEntry(entry))
1018 this.updateSubDirectories(false /* recursive */);
1019 var currentSequence = ++this.sequence_;
1020 var volumeInfo = this.volumeManager_.getVolumeInfo(entry);
1023 volumeInfo.resolveDisplayRoot(function() {
1024 if (this.sequence_ !== currentSequence)
1026 if (!this.searchAndSelectByEntry(entry))
1027 this.selectedItem = null;
1032 * Select the volume or the shortcut corresponding to the given index.
1033 * @param {number} index 0-based index of the target top-level item.
1034 * @return {boolean} True if one of the volume items is selected.
1036 DirectoryTree.prototype.selectByIndex = function(index) {
1037 if (index < 0 || index >= this.items.length)
1040 this.items[index].selected = true;
1045 * Retrieves the latest subdirectories and update them on the tree.
1047 * @param {boolean} recursive True if the update is recursively.
1048 * @param {function()=} opt_callback Called when subdirectories are fully
1051 DirectoryTree.prototype.updateSubDirectories = function(
1052 recursive, opt_callback) {
1053 this.redraw(recursive);
1060 * @param {boolean} recursive True if the update is recursively. False if the
1061 * only root items are updated.
1063 DirectoryTree.prototype.redraw = function(recursive) {
1064 this.updateSubElementsFromList(recursive);
1068 * Invoked when the filter is changed.
1071 DirectoryTree.prototype.onFilterChanged_ = function() {
1072 // Returns immediately, if the tree is hidden.
1076 this.redraw(true /* recursive */);
1080 * Invoked when a directory is changed.
1081 * @param {!UIEvent} event Event.
1084 DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
1085 if (event.eventType !== 'changed')
1088 for (var i = 0; i < this.items.length; i++) {
1089 if (this.items[i] instanceof VolumeItem)
1090 this.items[i].updateItemByEntry(event.entry);
1095 * Invoked when the current directory is changed.
1096 * @param {!UIEvent} event Event.
1099 DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
1100 this.selectByEntry(event.newDirEntry);
1104 * Invoked when the volume list or shortcut list is changed.
1107 DirectoryTree.prototype.onListContentChanged_ = function() {
1108 this.updateSubDirectories(false, function() {
1109 // If no item is selected now, try to select the item corresponding to
1110 // current directory because the current directory might have been populated
1111 // in this tree in previous updateSubDirectories().
1112 if (!this.selectedItem) {
1113 var currentDir = this.directoryModel_.getCurrentDirEntry();
1115 this.selectByEntry(currentDir);
1121 * Updates the UI after the layout has changed.
1123 DirectoryTree.prototype.relayout = function() {
1124 cr.dispatchSimpleEvent(this, 'relayout');