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>' +
217 '<div class="tree-children"></div>';
218 this.setAttribute('role', 'treeitem');
220 this.parentTree_ = tree;
221 this.directoryModel_ = tree.directoryModel;
222 this.parent_ = parentDirItem;
224 this.fullPath = path;
225 this.dirEntry_ = dirEntry;
226 this.fileFilter_ = this.directoryModel_.getFileFilter();
228 // Sets hasChildren=false tentatively. This will be overridden after
229 // scanning sub-directories in DirectoryTreeUtil.updateSubElementsFromList.
230 this.hasChildren = false;
232 this.addEventListener('expand', this.onExpand_.bind(this), false);
233 var icon = this.querySelector('.icon');
234 icon.classList.add('volume-icon');
235 var iconType = PathUtil.getRootType(path);
236 if (iconType && PathUtil.isRootPath(path))
237 icon.setAttribute('volume-type-icon', iconType);
239 icon.setAttribute('file-type-icon', 'folder');
241 if (this.parentTree_.contextMenuForSubitems)
242 this.setContextMenu(this.parentTree_.contextMenuForSubitems);
243 // Adds handler for future change.
244 this.parentTree_.addEventListener(
245 'contextMenuForSubitemsChange',
246 function(e) { this.setContextMenu(e.newValue); }.bind(this));
248 if (parentDirItem.expanded)
249 this.updateSubDirectories(false /* recursive */);
253 * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
254 * a complex layout. This call is not necessary, so we are ignoring it.
256 * @param {boolean} unused Unused.
259 DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
263 * Removes the child node, but without selecting the parent item, to avoid
264 * unintended changing of directories. Removing is done externally, and other
265 * code will navigate to another directory.
267 * @param {!cr.ui.TreeItem} child The tree item child to remove.
270 DirectoryItem.prototype.remove = function(child) {
271 this.lastElementChild.removeChild(child);
272 if (this.items.length == 0)
273 this.hasChildren = false;
277 * Invoked when the item is being expanded.
278 * @param {!UIEvent} e Event.
281 DirectoryItem.prototype.onExpand_ = function(e) {
282 this.updateSubDirectories(
283 true /* recursive */,
286 this.expanded = false;
293 * Retrieves the latest subdirectories and update them on the tree.
294 * @param {boolean} recursive True if the update is recursively.
295 * @param {function()=} opt_successCallback Callback called on success.
296 * @param {function()=} opt_errorCallback Callback called on error.
298 DirectoryItem.prototype.updateSubDirectories = function(
299 recursive, opt_successCallback, opt_errorCallback) {
300 if (util.isFakeEntry(this.entry)) {
301 if (opt_errorCallback)
306 var sortEntries = function(fileFilter, entries) {
307 entries.sort(function(a, b) {
308 return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1;
310 return entries.filter(fileFilter.filter.bind(fileFilter));
313 var onSuccess = function(entries) {
314 this.entries_ = entries;
315 this.redrawSubDirectoryList_(recursive);
316 opt_successCallback && opt_successCallback();
319 var reader = this.entry.createReader();
321 var readEntry = function() {
322 reader.readEntries(function(results) {
323 if (!results.length) {
324 onSuccess(sortEntries(this.fileFilter_, entries));
328 for (var i = 0; i < results.length; i++) {
329 var entry = results[i];
330 if (entry.isDirectory)
340 * Updates sub-elements of {@code parentElement} reading {@code DirectoryEntry}
341 * with calling {@code iterator}.
343 * @param {string} changedDirectryPath The path of the changed directory.
345 DirectoryItem.prototype.updateItemByPath = function(changedDirectryPath) {
346 if (changedDirectryPath === this.entry.fullPath) {
347 this.updateSubDirectories(false /* recursive */);
351 for (var i = 0; i < this.items.length; i++) {
352 var item = this.items[i];
353 if (PathUtil.isParentPath(item.entry.fullPath, changedDirectryPath)) {
354 item.updateItemByPath(changedDirectryPath);
361 * Redraw subitems with the latest information. The items are sorted in
362 * alphabetical order, case insensitive.
363 * @param {boolean} recursive True if the update is recursively.
366 DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
367 this.updateSubElementsFromList(recursive);
371 * Select the item corresponding to the given {@code entry}.
372 * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
374 DirectoryItem.prototype.selectByEntry = function(entry) {
375 if (util.isSameEntry(entry, this.entry)) {
376 this.selected = true;
380 if (this.searchAndSelectByEntry(entry))
383 // If the path doesn't exist, updates sub directories and tryes again.
384 this.updateSubDirectories(
385 false /* recursive */,
386 this.searchAndSelectByEntry.bind(this, entry));
390 * Executes the assigned action as a drop target.
392 DirectoryItem.prototype.doDropTargetAction = function() {
393 this.expanded = true;
397 * Executes the assigned action. DirectoryItem performs changeDirectory.
399 DirectoryItem.prototype.doAction = function() {
400 if (this.fullPath != this.directoryModel_.getCurrentDirPath())
401 this.directoryModel_.changeDirectory(this.fullPath);
405 * Sets the context menu for directory tree.
406 * @param {cr.ui.Menu} menu Menu to be set.
408 DirectoryItem.prototype.setContextMenu = function(menu) {
409 if (this.entry && PathUtil.isEligibleForFolderShortcut(this.entry.fullPath))
410 cr.ui.contextMenuHandler.setContextMenu(this, menu);
413 ////////////////////////////////////////////////////////////////////////////////
417 * Tree of directories on the middle bar. This element is also the root of
418 * items, in other words, this is the parent of the top-level items.
421 * @extends {cr.ui.Tree}
423 function DirectoryTree() {}
426 * Decorates an element.
427 * @param {HTMLElement} el Element to be DirectoryTree.
428 * @param {DirectoryModel} directoryModel Current DirectoryModel.
429 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
431 DirectoryTree.decorate = function(el, directoryModel, volumeManager) {
432 el.__proto__ = DirectoryTree.prototype;
433 (/** @type {DirectoryTree} */ el).decorate(directoryModel, volumeManager);
436 DirectoryTree.prototype = {
437 __proto__: cr.ui.Tree.prototype,
439 // DirectoryTree is always expanded.
440 get expanded() { return true; },
442 * @param {boolean} value Not used.
444 set expanded(value) {},
447 * The DirectoryEntry corresponding to this DirectoryItem. This may be
448 * a dummy DirectoryEntry.
449 * @type {DirectoryEntry|Object}
453 return this.dirEntry_;
457 * The DirectoryModel this tree corresponds to.
458 * @type {DirectoryModel}
460 get directoryModel() {
461 return this.directoryModel_;
465 * The VolumeManager instance of the system.
466 * @type {VolumeManager}
468 get volumeManager() {
469 return this.volumeManager_;
473 cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
476 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
478 * @param {boolean} recursive True if the all visible sub-directories are
479 * updated recursively including left arrows. If false, the update walks
480 * only immediate child directories without arrows.
482 DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
483 DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
487 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
488 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
490 * @return {boolean} True if the parent item is found.
492 DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
493 return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
497 * Decorates an element.
498 * @param {DirectoryModel} directoryModel Current DirectoryModel.
499 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
501 DirectoryTree.prototype.decorate = function(directoryModel, volumeManager) {
502 cr.ui.Tree.prototype.decorate.call(this);
504 this.directoryModel_ = directoryModel;
505 this.volumeManager_ = volumeManager;
506 this.entries_ = DirectoryTreeUtil.generateTopLevelEntries();
508 this.fileFilter_ = this.directoryModel_.getFileFilter();
509 this.fileFilter_.addEventListener('changed',
510 this.onFilterChanged_.bind(this));
512 this.directoryModel_.addEventListener('directory-changed',
513 this.onCurrentDirectoryChanged_.bind(this));
515 // Add a handler for directory change.
516 this.addEventListener('change', function() {
517 if (this.selectedItem &&
518 (!this.currentEntry_ ||
519 !util.isSameEntry(this.currentEntry_, this.selectedItem.entry))) {
520 this.currentEntry_ = this.selectedItem.entry;
521 this.selectedItem.doAction();
526 this.privateOnDirectoryChangedBound_ =
527 this.onDirectoryContentChanged_.bind(this);
528 chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
529 this.privateOnDirectoryChangedBound_);
531 this.scrollBar_ = MainPanelScrollBar();
532 this.scrollBar_.initialize(this.parentNode, this);
534 // Once, draws the list with the fake '/drive/' entry.
535 this.redraw(false /* recursive */);
536 // Resolves 'My Drive' entry and replaces the fake with the true one.
537 this.maybeResolveMyDriveRoot_(function() {
538 // After the true entry is resolved, draws the list again.
539 this.redraw(true /* recursive */);
544 * Select the item corresponding to the given entry.
545 * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
548 DirectoryTree.prototype.selectByEntry = function(entry) {
549 // If the target directory is not in the tree, do nothing.
550 if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath))
553 this.maybeResolveMyDriveRoot_(function() {
554 if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
557 if (this.searchAndSelectByEntry(entry))
560 this.selectedItem = null;
561 this.updateSubDirectories(
562 false /* recursive */,
563 // Success callback, failure is not handled.
565 if (!this.searchAndSelectByEntry(entry))
566 this.selectedItem = null;
572 * Resolves the My Drive root's entry, if it is a fake. If the entry is already
573 * resolved to a DirectoryEntry, completionCallback() will be called
575 * @param {function()} completionCallback Called when the resolving is
576 * done (or the entry is already resolved), regardless if it is
577 * successfully done or not.
580 DirectoryTree.prototype.maybeResolveMyDriveRoot_ = function(
581 completionCallback) {
582 var myDriveItem = this.items[0];
583 if (!util.isFakeEntry(myDriveItem.entry)) {
584 // The entry is already resolved. Don't need to try again.
585 completionCallback();
589 // The entry is a fake.
590 this.directoryModel_.resolveDirectory(
591 myDriveItem.fullPath,
593 if (!util.isFakeEntry(entry))
594 myDriveItem.dirEntry_ = entry;
596 completionCallback();
602 * Retrieves the latest subdirectories and update them on the tree.
603 * @param {boolean} recursive True if the update is recursively.
604 * @param {function()=} opt_successCallback Callback called on success.
605 * @param {function()=} opt_errorCallback Callback called on error.
607 DirectoryTree.prototype.updateSubDirectories = function(
608 recursive, opt_successCallback, opt_errorCallback) {
609 this.entries_ = DirectoryTreeUtil.generateTopLevelEntries();
610 this.redraw(recursive);
611 if (opt_successCallback)
612 opt_successCallback();
617 * @param {boolean} recursive True if the update is recursively. False if the
618 * only root items are updated.
620 DirectoryTree.prototype.redraw = function(recursive) {
621 this.updateSubElementsFromList(recursive);
625 * Invoked when the filter is changed.
628 DirectoryTree.prototype.onFilterChanged_ = function() {
629 // Returns immediately, if the tree is hidden.
633 this.redraw(true /* recursive */);
637 * Invoked when a directory is changed.
638 * @param {!UIEvent} event Event.
641 DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
642 if (event.eventType == 'changed') {
643 // TODO: Use Entry instead of urls. This will stop working once migrating
644 // to separate file systems. See: crbug.com/325052.
645 if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(event.entry.fullPath))
648 var myDriveItem = this.items[0];
649 myDriveItem.updateItemByPath(event.entry.fullPath);
654 * Invoked when the current directory is changed.
655 * @param {!UIEvent} event Event.
658 DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
659 this.selectByEntry(event.newDirEntry);
663 * Sets the margin height for the transparent preview panel at the bottom.
664 * @param {number} margin Margin to be set in px.
666 DirectoryTree.prototype.setBottomMarginForPanel = function(margin) {
667 this.style.paddingBottom = margin + 'px';
668 this.scrollBar_.setBottomMarginForPanel(margin);
672 * Updates the UI after the layout has changed.
674 DirectoryTree.prototype.relayout = function() {
675 cr.dispatchSimpleEvent(this, 'relayout');