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];
33 var locationInfo = tree.volumeManager_.getLocationInfo(currentEntry);
35 if (locationInfo && locationInfo.isRootEntry)
36 label = PathUtil.getRootTypeLabel(locationInfo.rootType);
38 label = currentEntry.name;
40 if (index >= this.items.length) {
41 var item = new DirectoryItem(label, currentEntry, this, tree);
44 } else if (util.isSameEntry(currentEntry, currentElement.entry)) {
45 if (recursive && this.expanded)
46 currentElement.updateSubDirectories(true /* recursive */);
49 } else if (currentEntry.toURL() < currentElement.entry.toURL()) {
50 var item = new DirectoryItem(label, currentEntry, this, tree);
51 this.addAt(item, index);
53 } else if (currentEntry.toURL() > currentElement.entry.toURL()) {
54 this.remove(currentElement);
59 while (removedChild = this.items[index]) {
60 this.remove(removedChild);
64 this.hasChildren = false;
65 this.expanded = false;
67 this.hasChildren = true;
72 * Finds a parent directory of the {@code entry} in {@code this}, and
73 * invokes the DirectoryItem.selectByEntry() of the found directory.
75 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
77 * @return {boolean} True if the parent item is found.
79 DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
80 for (var i = 0; i < this.items.length; i++) {
81 var item = this.items[i];
82 if (util.isDescendantEntry(item.entry, entry) ||
83 util.isSameEntry(item.entry, entry)) {
84 item.selectByEntry(entry);
91 Object.freeze(DirectoryItemTreeBaseMethods);
93 ////////////////////////////////////////////////////////////////////////////////
97 * A directory in the tree. Each element represents one directory.
99 * @param {string} label Label for this item.
100 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
101 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
102 * @param {DirectoryTree} tree Current tree, which contains this item.
103 * @extends {cr.ui.TreeItem}
106 function DirectoryItem(label, dirEntry, parentDirItem, tree) {
107 var item = new cr.ui.TreeItem();
108 DirectoryItem.decorate(item, label, dirEntry, parentDirItem, tree);
113 * @param {HTMLElement} el Element to be DirectoryItem.
114 * @param {string} label Label for this item.
115 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
116 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
117 * @param {DirectoryTree} tree Current tree, which contains this item.
119 DirectoryItem.decorate =
120 function(el, label, dirEntry, parentDirItem, tree) {
121 el.__proto__ = DirectoryItem.prototype;
122 (/** @type {DirectoryItem} */ el).decorate(
123 label, dirEntry, parentDirItem, tree);
126 DirectoryItem.prototype = {
127 __proto__: cr.ui.TreeItem.prototype,
130 * The DirectoryEntry corresponding to this DirectoryItem. This may be
131 * a dummy DirectoryEntry.
132 * @type {DirectoryEntry|Object}
135 return this.dirEntry_;
139 * The element containing the label text and the icon.
140 * @type {!HTMLElement}
144 return this.firstElementChild.querySelector('.label');
149 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
151 * @param {boolean} recursive True if the all visible sub-directories are
152 * updated recursively including left arrows. If false, the update walks
153 * only immediate child directories without arrows.
155 DirectoryItem.prototype.updateSubElementsFromList = function(recursive) {
156 DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
160 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
162 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
164 * @return {boolean} True if the parent item is found.
166 DirectoryItem.prototype.searchAndSelectByEntry = function(entry) {
167 return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
171 * @param {string} label Localized label for this item.
172 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
173 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
174 * @param {DirectoryTree} tree Current tree, which contains this item.
176 DirectoryItem.prototype.decorate = function(
177 label, dirEntry, parentDirItem, tree) {
179 '<div class="tree-row">' +
180 ' <span class="expand-icon"></span>' +
181 ' <span class="icon"></span>' +
182 ' <span class="label"></span>' +
184 '<div class="tree-children"></div>';
186 this.parentTree_ = tree;
187 this.directoryModel_ = tree.directoryModel;
188 this.parent_ = parentDirItem;
190 this.dirEntry_ = dirEntry;
191 this.fileFilter_ = this.directoryModel_.getFileFilter();
193 // Sets hasChildren=false tentatively. This will be overridden after
194 // scanning sub-directories in updateSubElementsFromList().
195 this.hasChildren = false;
197 this.addEventListener('expand', this.onExpand_.bind(this), false);
198 var icon = this.querySelector('.icon');
199 icon.classList.add('volume-icon');
200 var location = tree.volumeManager.getLocationInfo(dirEntry);
201 if (location && location.rootType && location.isRootEntry)
202 icon.setAttribute('volume-type-icon', location.rootType);
204 icon.setAttribute('file-type-icon', 'folder');
206 if (this.parentTree_.contextMenuForSubitems)
207 this.setContextMenu(this.parentTree_.contextMenuForSubitems);
208 // Adds handler for future change.
209 this.parentTree_.addEventListener(
210 'contextMenuForSubitemsChange',
211 function(e) { this.setContextMenu(e.newValue); }.bind(this));
213 if (parentDirItem.expanded)
214 this.updateSubDirectories(false /* recursive */);
218 * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
219 * a complex layout. This call is not necessary, so we are ignoring it.
221 * @param {boolean} unused Unused.
224 DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
228 * Removes the child node, but without selecting the parent item, to avoid
229 * unintended changing of directories. Removing is done externally, and other
230 * code will navigate to another directory.
232 * @param {!cr.ui.TreeItem} child The tree item child to remove.
235 DirectoryItem.prototype.remove = function(child) {
236 this.lastElementChild.removeChild(child);
237 if (this.items.length == 0)
238 this.hasChildren = false;
242 * Invoked when the item is being expanded.
243 * @param {!UIEvent} e Event.
246 DirectoryItem.prototype.onExpand_ = function(e) {
247 this.updateSubDirectories(
248 true /* recursive */,
251 this.expanded = false;
258 * Retrieves the latest subdirectories and update them on the tree.
259 * @param {boolean} recursive True if the update is recursively.
260 * @param {function()=} opt_successCallback Callback called on success.
261 * @param {function()=} opt_errorCallback Callback called on error.
263 DirectoryItem.prototype.updateSubDirectories = function(
264 recursive, opt_successCallback, opt_errorCallback) {
265 if (util.isFakeEntry(this.entry)) {
266 if (opt_errorCallback)
271 var sortEntries = function(fileFilter, entries) {
272 entries.sort(function(a, b) {
273 return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1;
275 return entries.filter(fileFilter.filter.bind(fileFilter));
278 var onSuccess = function(entries) {
279 this.entries_ = entries;
280 this.redrawSubDirectoryList_(recursive);
281 opt_successCallback && opt_successCallback();
284 var reader = this.entry.createReader();
286 var readEntry = function() {
287 reader.readEntries(function(results) {
288 if (!results.length) {
289 onSuccess(sortEntries(this.fileFilter_, entries));
293 for (var i = 0; i < results.length; i++) {
294 var entry = results[i];
295 if (entry.isDirectory)
305 * Searches for the changed directory in the current subtree, and if it is found
308 * @param {DirectoryEntry} changedDirectoryEntry The entry ot the changed
311 DirectoryItem.prototype.updateItemByEntry = function(changedDirectoryEntry) {
312 if (util.isSameEntry(changedDirectoryEntry, this.entry)) {
313 this.updateSubDirectories(false /* recursive */);
317 // Traverse the entire subtree to find the changed element.
318 for (var i = 0; i < this.items.length; i++) {
319 var item = this.items[i];
320 if (util.isDescendantEntry(item.entry, changedDirectoryEntry)) {
321 item.updateItemByEntry(changedDirectoryEntry);
328 * Redraw subitems with the latest information. The items are sorted in
329 * alphabetical order, case insensitive.
330 * @param {boolean} recursive True if the update is recursively.
333 DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
334 this.updateSubElementsFromList(recursive);
338 * Select the item corresponding to the given {@code entry}.
339 * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
341 DirectoryItem.prototype.selectByEntry = function(entry) {
342 if (util.isSameEntry(entry, this.entry)) {
343 this.selected = true;
347 if (this.searchAndSelectByEntry(entry))
350 // If the entry doesn't exist, updates sub directories and tries again.
351 this.updateSubDirectories(
352 false /* recursive */,
353 this.searchAndSelectByEntry.bind(this, entry));
357 * Executes the assigned action as a drop target.
359 DirectoryItem.prototype.doDropTargetAction = function() {
360 this.expanded = true;
364 * Executes the assigned action. DirectoryItem performs changeDirectory.
366 DirectoryItem.prototype.doAction = function() {
367 if (!this.directoryModel_.getCurrentDirEntry() ||
368 !util.isSameEntry(this.entry,
369 this.directoryModel_.getCurrentDirEntry())) {
370 this.directoryModel_.changeDirectoryEntry(this.entry);
375 * Sets the context menu for directory tree.
376 * @param {cr.ui.Menu} menu Menu to be set.
378 DirectoryItem.prototype.setContextMenu = function(menu) {
379 var tree = this.parentTree_ || this; // If no parent, 'this' itself is tree.
380 var locationInfo = tree.volumeManager_.getLocationInfo(this.entry);
381 if (locationInfo && locationInfo.isEligibleForFolderShortcut)
382 cr.ui.contextMenuHandler.setContextMenu(this, menu);
385 ////////////////////////////////////////////////////////////////////////////////
389 * Tree of directories on the middle bar. This element is also the root of
390 * items, in other words, this is the parent of the top-level items.
393 * @extends {cr.ui.Tree}
395 function DirectoryTree() {}
398 * Decorates an element.
399 * @param {HTMLElement} el Element to be DirectoryTree.
400 * @param {DirectoryModel} directoryModel Current DirectoryModel.
401 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
403 DirectoryTree.decorate = function(el, directoryModel, volumeManager) {
404 el.__proto__ = DirectoryTree.prototype;
405 (/** @type {DirectoryTree} */ el).decorate(directoryModel, volumeManager);
408 DirectoryTree.prototype = {
409 __proto__: cr.ui.Tree.prototype,
411 // DirectoryTree is always expanded.
412 get expanded() { return true; },
414 * @param {boolean} value Not used.
416 set expanded(value) {},
419 * The DirectoryEntry corresponding to this DirectoryItem. This may be
420 * a dummy DirectoryEntry.
421 * @type {DirectoryEntry|Object}
425 return this.dirEntry_;
429 * The DirectoryModel this tree corresponds to.
430 * @type {DirectoryModel}
432 get directoryModel() {
433 return this.directoryModel_;
437 * The VolumeManager instance of the system.
438 * @type {VolumeManager}
440 get volumeManager() {
441 return this.volumeManager_;
445 cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
448 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
450 * @param {boolean} recursive True if the all visible sub-directories are
451 * updated recursively including left arrows. If false, the update walks
452 * only immediate child directories without arrows.
454 DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
455 DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
459 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
461 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
463 * @return {boolean} True if the parent item is found.
465 DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
466 return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
470 * Decorates an element.
471 * @param {DirectoryModel} directoryModel Current DirectoryModel.
472 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
474 DirectoryTree.prototype.decorate = function(directoryModel, volumeManager) {
475 cr.ui.Tree.prototype.decorate.call(this);
478 this.directoryModel_ = directoryModel;
479 this.volumeManager_ = volumeManager;
481 this.currentVolumeInfo_ = null;
483 this.fileFilter_ = this.directoryModel_.getFileFilter();
484 this.fileFilter_.addEventListener('changed',
485 this.onFilterChanged_.bind(this));
487 this.directoryModel_.addEventListener('directory-changed',
488 this.onCurrentDirectoryChanged_.bind(this));
490 // Add a handler for directory change.
491 this.addEventListener('change', function() {
492 if (this.selectedItem &&
493 (!this.currentEntry_ ||
494 !util.isSameEntry(this.currentEntry_, this.selectedItem.entry))) {
495 this.currentEntry_ = this.selectedItem.entry;
496 this.selectedItem.doAction();
501 this.privateOnDirectoryChangedBound_ =
502 this.onDirectoryContentChanged_.bind(this);
503 chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
504 this.privateOnDirectoryChangedBound_);
506 this.scrollBar_ = MainPanelScrollBar();
507 this.scrollBar_.initialize(this.parentNode, this);
511 * Select the item corresponding to the given entry.
512 * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
515 DirectoryTree.prototype.selectByEntry = function(entry) {
516 // If the target directory is not in the tree, do nothing.
517 var locationInfo = this.volumeManager_.getLocationInfo(entry);
518 if (!locationInfo || !locationInfo.isDriveBased)
521 var volumeInfo = this.volumeManager_.getVolumeInfo(entry);
522 if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
525 if (this.searchAndSelectByEntry(entry))
528 this.updateSubDirectories(false /* recursive */);
529 var currentSequence = ++this.sequence_;
530 volumeInfo.resolveDisplayRoot(function() {
531 if (this.sequence_ !== currentSequence)
533 if (!this.searchAndSelectByEntry(entry))
534 this.selectedItem = null;
539 * Retrieves the latest subdirectories and update them on the tree.
541 * @param {boolean} recursive True if the update is recursively.
542 * @param {function()=} opt_callback Called when subdirectories are fully
545 DirectoryTree.prototype.updateSubDirectories = function(
546 recursive, opt_callback) {
547 var callback = opt_callback || function() {};
550 var compareEntries = function(a, b) {
551 return a.toURL() < b.toURL();
554 // Add fakes (if any).
555 for (var key in this.currentVolumeInfo_.fakeEntries) {
556 this.entries_.push(this.currentVolumeInfo_.fakeEntries[key]);
559 // If the display root is not available yet, then redraw anyway with what
560 // we have. However, concurrently try to resolve the display root and then
562 if (!this.currentVolumeInfo_.displayRoot) {
563 this.entries_.sort(compareEntries);
564 this.redraw(recursive);
567 this.currentVolumeInfo_.resolveDisplayRoot(function(displayRoot) {
568 this.entries_.push(this.currentVolumeInfo_.displayRoot);
569 this.entries_.sort(compareEntries);
570 this.redraw(recursive); // Redraw.
572 }.bind(this), callback /* Ignore errors. */);
577 * @param {boolean} recursive True if the update is recursively. False if the
578 * only root items are updated.
580 DirectoryTree.prototype.redraw = function(recursive) {
581 this.updateSubElementsFromList(recursive);
585 * Invoked when the filter is changed.
588 DirectoryTree.prototype.onFilterChanged_ = function() {
589 // Returns immediately, if the tree is hidden.
593 this.redraw(true /* recursive */);
597 * Invoked when a directory is changed.
598 * @param {!UIEvent} event Event.
601 DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
602 if (event.eventType !== 'changed')
605 var locationInfo = this.volumeManager_.getLocationInfo(event.entry);
606 if (!locationInfo || !locationInfo.isDriveBased)
609 var myDriveItem = this.items[0];
611 myDriveItem.updateItemByEntry(event.entry);
615 * Invoked when the current directory is changed.
616 * @param {!UIEvent} event Event.
619 DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
620 this.currentVolumeInfo_ =
621 this.volumeManager_.getVolumeInfo(event.newDirEntry);
622 this.selectByEntry(event.newDirEntry);
626 * Sets the margin height for the transparent preview panel at the bottom.
627 * @param {number} margin Margin to be set in px.
629 DirectoryTree.prototype.setBottomMarginForPanel = function(margin) {
630 this.style.paddingBottom = margin + 'px';
631 this.scrollBar_.setBottomMarginForPanel(margin);
635 * Updates the UI after the layout has changed.
637 DirectoryTree.prototype.relayout = function() {
638 cr.dispatchSimpleEvent(this, 'relayout');