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 * Utility methods. They are intended for use only in this file.
13 var DirectoryTreeUtil = {};
16 * Generate a list of the directory entries for the top level on the tree.
17 * @return {Array.<DirectoryEntry>} Entries for the top level on the tree.
19 DirectoryTreeUtil.generateTopLevelEntries = function() {
21 DirectoryModel.fakeDriveEntry_,
22 DirectoryModel.fakeDriveOfflineEntry_,
23 DirectoryModel.fakeDriveSharedWithMeEntry_,
24 DirectoryModel.fakeDriveRecentEntry_,
27 for (var i = 0; i < entries.length; i++) {
28 entries[i]['label'] = PathUtil.getRootLabel(entries[i].fullPath);
35 * Checks if the given directory can be on the tree or not.
37 * @param {string} path Path to be checked.
38 * @return {boolean} True if the path is eligible for the directory tree.
41 DirectoryTreeUtil.isEligiblePathForDirectoryTree = function(path) {
42 return PathUtil.isDriveBasedPath(path);
45 Object.freeze(DirectoryTreeUtil);
47 ////////////////////////////////////////////////////////////////////////////////
51 * Implementation of methods for DirectoryTree and DirectoryItem. These classes
52 * inherits cr.ui.Tree/TreeItem so we can't make them inherit this class.
53 * Instead, we separate their implementations to this separate object and call
54 * it with setting 'this' from DirectoryTree/Item.
56 var DirectoryItemTreeBaseMethods = {};
59 * Updates sub-elements of {@code this} reading {@code DirectoryEntry}.
60 * The list of {@code DirectoryEntry} are not updated by this method.
62 * @param {boolean} recursive True if the all visible sub-directories are
63 * updated recursively including left arrows. If false, the update walks
64 * only immediate child directories without arrows.
66 DirectoryItemTreeBaseMethods.updateSubElementsFromList = function(recursive) {
68 var tree = this.parentTree_ || this; // If no parent, 'this' itself is tree.
69 while (this.entries_[index]) {
70 var currentEntry = this.entries_[index];
71 var currentElement = this.items[index];
73 if (index >= this.items.length) {
74 var item = new DirectoryItem(currentEntry, this, tree);
77 } else if (currentEntry.fullPath == currentElement.fullPath) {
78 if (recursive && this.expanded)
79 currentElement.updateSubDirectories(true /* recursive */);
82 } else if (currentEntry.fullPath < currentElement.fullPath) {
83 var item = new DirectoryItem(currentEntry, this, tree);
84 this.addAt(item, index);
86 } else if (currentEntry.fullPath > currentElement.fullPath) {
87 this.remove(currentElement);
92 while (removedChild = this.items[index]) {
93 this.remove(removedChild);
97 this.hasChildren = false;
98 this.expanded = false;
100 this.hasChildren = true;
105 * Finds a parent directory of the {@code entry} in {@code this}, and
106 * invokes the DirectoryItem.selectByEntry() of the found directory.
108 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
110 * @return {boolean} True if the parent item is found.
112 DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
113 for (var i = 0; i < this.items.length; i++) {
114 var item = this.items[i];
115 if (util.isParentEntry(item.entry, entry)) {
116 item.selectByEntry(entry);
123 Object.freeze(DirectoryItemTreeBaseMethods);
125 ////////////////////////////////////////////////////////////////////////////////
129 * A directory in the tree. Each element represents one directory.
131 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
132 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
133 * @param {DirectoryTree} tree Current tree, which contains this item.
134 * @extends {cr.ui.TreeItem}
137 function DirectoryItem(dirEntry, parentDirItem, tree) {
138 var item = cr.doc.createElement('div');
139 DirectoryItem.decorate(item, dirEntry, parentDirItem, tree);
144 * @param {HTMLElement} el Element to be DirectoryItem.
145 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
146 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
147 * @param {DirectoryTree} tree Current tree, which contains this item.
149 DirectoryItem.decorate =
150 function(el, dirEntry, parentDirItem, tree) {
151 el.__proto__ = DirectoryItem.prototype;
152 (/** @type {DirectoryItem} */ el).decorate(
153 dirEntry, parentDirItem, tree);
156 DirectoryItem.prototype = {
157 __proto__: cr.ui.TreeItem.prototype,
160 * The DirectoryEntry corresponding to this DirectoryItem. This may be
161 * a dummy DirectoryEntry.
162 * @type {DirectoryEntry|Object}
165 return this.dirEntry_;
169 * The element containing the label text and the icon.
170 * @type {!HTMLElement}
174 return this.firstElementChild.querySelector('.label');
179 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
181 * @param {boolean} recursive True if the all visible sub-directories are
182 * updated recursively including left arrows. If false, the update walks
183 * only immediate child directories without arrows.
185 DirectoryItem.prototype.updateSubElementsFromList = function(recursive) {
186 DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
190 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
191 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
193 * @return {boolean} True if the parent item is found.
195 DirectoryItem.prototype.searchAndSelectByEntry = function(entry) {
196 return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
200 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
201 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
202 * @param {DirectoryTree} tree Current tree, which contains this item.
204 DirectoryItem.prototype.decorate = function(
205 dirEntry, parentDirItem, tree) {
206 var path = dirEntry.fullPath;
208 label = dirEntry.label ? dirEntry.label : dirEntry.name;
210 this.className = 'tree-item';
212 '<div class="tree-row">' +
213 ' <span class="expand-icon"></span>' +
214 ' <span class="icon"></span>' +
215 ' <span class="label"></span>' +
216 ' <div class="root-eject"></div>' +
218 '<div class="tree-children"></div>';
219 this.setAttribute('role', 'treeitem');
221 this.parentTree_ = tree;
222 this.directoryModel_ = tree.directoryModel;
223 this.parent_ = parentDirItem;
225 this.fullPath = path;
226 this.dirEntry_ = dirEntry;
227 this.fileFilter_ = this.directoryModel_.getFileFilter();
229 // Sets hasChildren=false tentatively. This will be overridden after
230 // scanning sub-directories in DirectoryTreeUtil.updateSubElementsFromList.
231 this.hasChildren = false;
233 this.addEventListener('expand', this.onExpand_.bind(this), false);
234 var icon = this.querySelector('.icon');
235 icon.classList.add('volume-icon');
236 var iconType = PathUtil.getRootType(path);
237 if (iconType && PathUtil.isRootPath(path))
238 icon.setAttribute('volume-type-icon', iconType);
240 icon.setAttribute('file-type-icon', 'folder');
242 var eject = this.querySelector('.root-eject');
243 eject.hidden = !PathUtil.isUnmountableByUser(path);
244 eject.addEventListener('click',
246 event.stopPropagation();
247 if (!PathUtil.isUnmountableByUser(path))
250 tree.volumeManager.unmount(path, function() {}, function() {});
253 if (this.parentTree_.contextMenuForSubitems)
254 this.setContextMenu(this.parentTree_.contextMenuForSubitems);
255 // Adds handler for future change.
256 this.parentTree_.addEventListener(
257 'contextMenuForSubitemsChange',
258 function(e) { this.setContextMenu(e.newValue); }.bind(this));
260 if (parentDirItem.expanded)
261 this.updateSubDirectories(false /* recursive */);
265 * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
266 * a complex layout. This call is not necessary, so we are ignoring it.
268 * @param {boolean} unused Unused.
271 DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
275 * Removes the child node, but without selecting the parent item, to avoid
276 * unintended changing of directories. Removing is done externally, and other
277 * code will navigate to another directory.
279 * @param {!cr.ui.TreeItem} child The tree item child to remove.
282 DirectoryItem.prototype.remove = function(child) {
283 this.lastElementChild.removeChild(child);
284 if (this.items.length == 0)
285 this.hasChildren = false;
289 * Invoked when the item is being expanded.
290 * @param {!UIEvent} e Event.
293 DirectoryItem.prototype.onExpand_ = function(e) {
294 this.updateSubDirectories(
295 true /* recursive */,
298 this.expanded = false;
305 * Retrieves the latest subdirectories and update them on the tree.
306 * @param {boolean} recursive True if the update is recursively.
307 * @param {function()=} opt_successCallback Callback called on success.
308 * @param {function()=} opt_errorCallback Callback called on error.
310 DirectoryItem.prototype.updateSubDirectories = function(
311 recursive, opt_successCallback, opt_errorCallback) {
312 if (util.isFakeDirectoryEntry(this.entry)) {
313 if (opt_errorCallback)
318 var sortEntries = function(fileFilter, entries) {
319 entries.sort(function(a, b) {
320 return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1;
322 return entries.filter(fileFilter.filter.bind(fileFilter));
325 var onSuccess = function(entries) {
326 this.entries_ = entries;
327 this.redrawSubDirectoryList_(recursive);
328 opt_successCallback && opt_successCallback();
331 var reader = this.entry.createReader();
333 var readEntry = function() {
334 reader.readEntries(function(results) {
335 if (!results.length) {
336 onSuccess(sortEntries(this.fileFilter_, entries));
340 for (var i = 0; i < results.length; i++) {
341 var entry = results[i];
342 if (entry.isDirectory)
352 * Updates sub-elements of {@code parentElement} reading {@code DirectoryEntry}
353 * with calling {@code iterator}.
355 * @param {string} changedDirectryPath The path of the changed directory.
357 DirectoryItem.prototype.updateItemByPath = function(changedDirectryPath) {
358 if (changedDirectryPath === this.entry.fullPath) {
359 this.updateSubDirectories(false /* recursive */);
363 for (var i = 0; i < this.items.length; i++) {
364 var item = this.items[i];
365 if (PathUtil.isParentPath(item.entry.fullPath, changedDirectryPath)) {
366 item.updateItemByPath(changedDirectryPath);
373 * Redraw subitems with the latest information. The items are sorted in
374 * alphabetical order, case insensitive.
375 * @param {boolean} recursive True if the update is recursively.
378 DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
379 this.updateSubElementsFromList(recursive);
383 * Select the item corresponding to the given {@code entry}.
384 * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
386 DirectoryItem.prototype.selectByEntry = function(entry) {
387 if (util.isSameEntry(entry, this.entry)) {
388 this.selected = true;
392 if (this.searchAndSelectByEntry(entry))
395 // If the path doesn't exist, updates sub directories and tryes again.
396 this.updateSubDirectories(
397 false /* recursive */,
398 this.searchAndSelectByEntry.bind(this, entry));
402 * Executes the assigned action as a drop target.
404 DirectoryItem.prototype.doDropTargetAction = function() {
405 this.expanded = true;
409 * Executes the assigned action. DirectoryItem performs changeDirectory.
411 DirectoryItem.prototype.doAction = function() {
412 if (this.fullPath != this.directoryModel_.getCurrentDirPath())
413 this.directoryModel_.changeDirectory(this.fullPath);
417 * Sets the context menu for directory tree.
418 * @param {cr.ui.Menu} menu Menu to be set.
420 DirectoryItem.prototype.setContextMenu = function(menu) {
421 if (this.entry && PathUtil.isEligibleForFolderShortcut(this.entry.fullPath))
422 cr.ui.contextMenuHandler.setContextMenu(this, menu);
425 ////////////////////////////////////////////////////////////////////////////////
429 * Tree of directories on the middle bar. This element is also the root of
430 * items, in other words, this is the parent of the top-level items.
433 * @extends {cr.ui.Tree}
435 function DirectoryTree() {}
438 * Decorates an element.
439 * @param {HTMLElement} el Element to be DirectoryTree.
440 * @param {DirectoryModel} directoryModel Current DirectoryModel.
441 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
443 DirectoryTree.decorate = function(el, directoryModel, volumeManager) {
444 el.__proto__ = DirectoryTree.prototype;
445 (/** @type {DirectoryTree} */ el).decorate(directoryModel, volumeManager);
448 DirectoryTree.prototype = {
449 __proto__: cr.ui.Tree.prototype,
451 // DirectoryTree is always expanded.
452 get expanded() { return true; },
454 * @param {boolean} value Not used.
456 set expanded(value) {},
459 * The DirectoryEntry corresponding to this DirectoryItem. This may be
460 * a dummy DirectoryEntry.
461 * @type {DirectoryEntry|Object}
465 return this.dirEntry_;
469 * The DirectoryModel this tree corresponds to.
470 * @type {DirectoryModel}
472 get directoryModel() {
473 return this.directoryModel_;
477 * The VolumeManager instance of the system.
478 * @type {VolumeManager}
480 get volumeManager() {
481 return this.volumeManager_;
485 cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
488 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
490 * @param {boolean} recursive True if the all visible sub-directories are
491 * updated recursively including left arrows. If false, the update walks
492 * only immediate child directories without arrows.
494 DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
495 DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
499 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
500 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
502 * @return {boolean} True if the parent item is found.
504 DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
505 return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
509 * Decorates an element.
510 * @param {DirectoryModel} directoryModel Current DirectoryModel.
511 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
513 DirectoryTree.prototype.decorate = function(directoryModel, volumeManager) {
514 cr.ui.Tree.prototype.decorate.call(this);
516 this.directoryModel_ = directoryModel;
517 this.volumeManager_ = volumeManager;
518 this.entries_ = DirectoryTreeUtil.generateTopLevelEntries();
520 this.fileFilter_ = this.directoryModel_.getFileFilter();
521 this.fileFilter_.addEventListener('changed',
522 this.onFilterChanged_.bind(this));
524 this.directoryModel_.addEventListener('directory-changed',
525 this.onCurrentDirectoryChanged_.bind(this));
527 // Add a handler for directory change.
528 this.addEventListener('change', function() {
529 if (this.selectedItem &&
530 (!this.currentEntry_ ||
531 !util.isSameEntry(this.currentEntry_, this.selectedItem.entry))) {
532 this.currentEntry_ = this.selectedItem.entry;
533 this.selectedItem.doAction();
538 this.privateOnDirectoryChangedBound_ =
539 this.onDirectoryContentChanged_.bind(this);
540 chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
541 this.privateOnDirectoryChangedBound_);
543 this.scrollBar_ = MainPanelScrollBar();
544 this.scrollBar_.initialize(this.parentNode, this);
546 // Once, draws the list with the fake '/drive/' entry.
547 this.redraw(false /* recursive */);
548 // Resolves 'My Drive' entry and replaces the fake with the true one.
549 this.maybeResolveMyDriveRoot_(function() {
550 // After the true entry is resolved, draws the list again.
551 this.redraw(true /* recursive */);
556 * Select the item corresponding to the given entry.
557 * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
560 DirectoryTree.prototype.selectByEntry = function(entry) {
561 // If the target directory is not in the tree, do nothing.
562 if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath))
565 this.maybeResolveMyDriveRoot_(function() {
566 if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
569 if (this.searchAndSelectByEntry(entry))
572 this.selectedItem = null;
573 this.updateSubDirectories(
574 false /* recursive */,
575 // Success callback, failure is not handled.
577 if (!this.searchAndSelectByEntry(entry))
578 this.selectedItem = null;
584 * Resolves the My Drive root's entry, if it is a fake. If the entry is already
585 * resolved to a DirectoryEntry, completionCallback() will be called
587 * @param {function()} completionCallback Called when the resolving is
588 * done (or the entry is already resolved), regardless if it is
589 * successfully done or not.
592 DirectoryTree.prototype.maybeResolveMyDriveRoot_ = function(
593 completionCallback) {
594 var myDriveItem = this.items[0];
595 if (!util.isFakeDirectoryEntry(myDriveItem.entry)) {
596 // The entry is already resolved. Don't need to try again.
597 completionCallback();
601 // The entry is a fake.
602 this.directoryModel_.resolveDirectory(
603 myDriveItem.fullPath,
605 if (!util.isFakeDirectoryEntry(entry)) {
606 myDriveItem.dirEntry_ = entry;
609 completionCallback();
615 * Retrieves the latest subdirectories and update them on the tree.
616 * @param {boolean} recursive True if the update is recursively.
617 * @param {function()=} opt_successCallback Callback called on success.
618 * @param {function()=} opt_errorCallback Callback called on error.
620 DirectoryTree.prototype.updateSubDirectories = function(
621 recursive, opt_successCallback, opt_errorCallback) {
622 this.entries_ = DirectoryTreeUtil.generateTopLevelEntries();
623 this.redraw(recursive);
624 if (opt_successCallback)
625 opt_successCallback();
630 * @param {boolean} recursive True if the update is recursively. False if the
631 * only root items are updated.
633 DirectoryTree.prototype.redraw = function(recursive) {
634 this.updateSubElementsFromList(recursive);
638 * Invoked when the filter is changed.
641 DirectoryTree.prototype.onFilterChanged_ = function() {
642 // Returns immediately, if the tree is hidden.
646 this.redraw(true /* recursive */);
650 * Invoked when a directory is changed.
651 * @param {!UIEvent} event Event.
654 DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
655 if (event.eventType == 'changed') {
656 var path = util.extractFilePath(event.directoryUrl);
657 if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(path))
660 var myDriveItem = this.items[0];
661 myDriveItem.updateItemByPath(path);
666 * Invoked when the current directory is changed.
667 * @param {!UIEvent} event Event.
670 DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
671 this.selectByEntry(event.newDirEntry);
675 * Sets the margin height for the transparent preview panel at the bottom.
676 * @param {number} margin Margin to be set in px.
678 DirectoryTree.prototype.setBottomMarginForPanel = function(margin) {
679 this.style.paddingBottom = margin + 'px';
680 this.scrollBar_.setBottomMarginForPanel(margin);
684 * Updates the UI after the layout has changed.
686 DirectoryTree.prototype.relayout = function() {
687 cr.dispatchSimpleEvent(this, 'relayout');