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 if (recursive && this.expanded)
40 currentElement.updateSubDirectories(true /* recursive */);
43 } else if (currentEntry.toURL() < currentElement.entry.toURL()) {
44 var item = new DirectoryItem(label, currentEntry, this, tree);
45 this.addAt(item, index);
47 } else if (currentEntry.toURL() > currentElement.entry.toURL()) {
48 this.remove(currentElement);
53 while (removedChild = this.items[index]) {
54 this.remove(removedChild);
58 this.hasChildren = false;
59 this.expanded = false;
61 this.hasChildren = true;
66 * Finds a parent directory of the {@code entry} in {@code this}, and
67 * invokes the DirectoryItem.selectByEntry() of the found directory.
69 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
71 * @return {boolean} True if the parent item is found.
73 DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
74 for (var i = 0; i < this.items.length; i++) {
75 var item = this.items[i];
76 if (util.isDescendantEntry(item.entry, entry) ||
77 util.isSameEntry(item.entry, entry)) {
78 item.selectByEntry(entry);
85 Object.freeze(DirectoryItemTreeBaseMethods);
87 ////////////////////////////////////////////////////////////////////////////////
91 * A directory in the tree. Each element represents one directory.
93 * @param {string} label Label for this item.
94 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
95 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
96 * @param {DirectoryTree} tree Current tree, which contains this item.
97 * @extends {cr.ui.TreeItem}
100 function DirectoryItem(label, dirEntry, parentDirItem, tree) {
101 var item = new cr.ui.TreeItem();
102 DirectoryItem.decorate(item, label, dirEntry, parentDirItem, tree);
107 * @param {HTMLElement} el Element to be DirectoryItem.
108 * @param {string} label Label for this item.
109 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
110 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
111 * @param {DirectoryTree} tree Current tree, which contains this item.
113 DirectoryItem.decorate =
114 function(el, label, dirEntry, parentDirItem, tree) {
115 el.__proto__ = DirectoryItem.prototype;
116 (/** @type {DirectoryItem} */ el).decorate(
117 label, dirEntry, parentDirItem, tree);
120 DirectoryItem.prototype = {
121 __proto__: cr.ui.TreeItem.prototype,
124 * The DirectoryEntry corresponding to this DirectoryItem. This may be
125 * a dummy DirectoryEntry.
126 * @type {DirectoryEntry|Object}
129 return this.dirEntry_;
133 * The element containing the label text and the icon.
134 * @type {!HTMLElement}
138 return this.firstElementChild.querySelector('.label');
143 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
145 * @param {boolean} recursive True if the all visible sub-directories are
146 * updated recursively including left arrows. If false, the update walks
147 * only immediate child directories without arrows.
149 DirectoryItem.prototype.updateSubElementsFromList = function(recursive) {
150 DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
154 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
156 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
158 * @return {boolean} True if the parent item is found.
160 DirectoryItem.prototype.searchAndSelectByEntry = function(entry) {
161 return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
165 * @param {string} label Localized label for this item.
166 * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
167 * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
168 * @param {DirectoryTree} tree Current tree, which contains this item.
170 DirectoryItem.prototype.decorate = function(
171 label, dirEntry, parentDirItem, tree) {
173 '<div class="tree-row">' +
174 ' <span class="expand-icon"></span>' +
175 ' <span class="icon"></span>' +
176 ' <span class="label"></span>' +
178 '<div class="tree-children"></div>';
180 this.parentTree_ = tree;
181 this.directoryModel_ = tree.directoryModel;
182 this.parent_ = parentDirItem;
184 this.dirEntry_ = dirEntry;
185 this.fileFilter_ = this.directoryModel_.getFileFilter();
187 // Sets hasChildren=false tentatively. This will be overridden after
188 // scanning sub-directories in updateSubElementsFromList().
189 this.hasChildren = false;
191 this.addEventListener('expand', this.onExpand_.bind(this), false);
192 var icon = this.querySelector('.icon');
193 icon.classList.add('volume-icon');
194 var location = tree.volumeManager.getLocationInfo(dirEntry);
195 if (location && location.rootType && location.isRootEntry)
196 icon.setAttribute('volume-type-icon', location.rootType);
198 icon.setAttribute('file-type-icon', 'folder');
200 if (this.parentTree_.contextMenuForSubitems)
201 this.setContextMenu(this.parentTree_.contextMenuForSubitems);
202 // Adds handler for future change.
203 this.parentTree_.addEventListener(
204 'contextMenuForSubitemsChange',
205 function(e) { this.setContextMenu(e.newValue); }.bind(this));
207 if (parentDirItem.expanded)
208 this.updateSubDirectories(false /* recursive */);
212 * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
213 * a complex layout. This call is not necessary, so we are ignoring it.
215 * @param {boolean} unused Unused.
218 DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
222 * Removes the child node, but without selecting the parent item, to avoid
223 * unintended changing of directories. Removing is done externally, and other
224 * code will navigate to another directory.
226 * @param {!cr.ui.TreeItem} child The tree item child to remove.
229 DirectoryItem.prototype.remove = function(child) {
230 this.lastElementChild.removeChild(child);
231 if (this.items.length == 0)
232 this.hasChildren = false;
236 * Invoked when the item is being expanded.
237 * @param {!UIEvent} e Event.
240 DirectoryItem.prototype.onExpand_ = function(e) {
241 this.updateSubDirectories(
242 true /* recursive */,
245 this.expanded = false;
252 * Invoked when the tree item is clicked.
254 * @param {Event} e Click event.
257 DirectoryItem.prototype.handleClick = function(e) {
258 cr.ui.TreeItem.prototype.handleClick.call(this, e);
260 if (e.target.classList.contains('expand-icon'))
263 var currentDirectoryEntry = this.directoryModel_.getCurrentDirEntry();
264 if (currentDirectoryEntry &&
265 util.isSameEntry(this.entry, currentDirectoryEntry)) {
266 // On clicking the current directory, clears the selection on the file list.
267 this.directoryModel_.clearSelection();
269 // Otherwise, changes the current directory.
270 this.directoryModel_.changeDirectoryEntry(this.entry);
275 * Retrieves the latest subdirectories and update them on the tree.
276 * @param {boolean} recursive True if the update is recursively.
277 * @param {function()=} opt_successCallback Callback called on success.
278 * @param {function()=} opt_errorCallback Callback called on error.
280 DirectoryItem.prototype.updateSubDirectories = function(
281 recursive, opt_successCallback, opt_errorCallback) {
282 if (util.isFakeEntry(this.entry)) {
283 if (opt_errorCallback)
288 var sortEntries = function(fileFilter, entries) {
289 entries.sort(function(a, b) {
290 return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1;
292 return entries.filter(fileFilter.filter.bind(fileFilter));
295 var onSuccess = function(entries) {
296 this.entries_ = entries;
297 this.redrawSubDirectoryList_(recursive);
298 opt_successCallback && opt_successCallback();
301 var reader = this.entry.createReader();
303 var readEntry = function() {
304 reader.readEntries(function(results) {
305 if (!results.length) {
306 onSuccess(sortEntries(this.fileFilter_, entries));
310 for (var i = 0; i < results.length; i++) {
311 var entry = results[i];
312 if (entry.isDirectory)
322 * Searches for the changed directory in the current subtree, and if it is found
325 * @param {DirectoryEntry} changedDirectoryEntry The entry ot the changed
328 DirectoryItem.prototype.updateItemByEntry = function(changedDirectoryEntry) {
329 if (util.isSameEntry(changedDirectoryEntry, this.entry)) {
330 this.updateSubDirectories(false /* recursive */);
334 // Traverse the entire subtree to find the changed element.
335 for (var i = 0; i < this.items.length; i++) {
336 var item = this.items[i];
337 if (util.isDescendantEntry(item.entry, changedDirectoryEntry)) {
338 item.updateItemByEntry(changedDirectoryEntry);
345 * Redraw subitems with the latest information. The items are sorted in
346 * alphabetical order, case insensitive.
347 * @param {boolean} recursive True if the update is recursively.
350 DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
351 this.updateSubElementsFromList(recursive);
355 * Select the item corresponding to the given {@code entry}.
356 * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
358 DirectoryItem.prototype.selectByEntry = function(entry) {
359 if (util.isSameEntry(entry, this.entry)) {
360 this.selected = true;
364 if (this.searchAndSelectByEntry(entry))
367 // If the entry doesn't exist, updates sub directories and tries again.
368 this.updateSubDirectories(
369 false /* recursive */,
370 this.searchAndSelectByEntry.bind(this, entry));
374 * Executes the assigned action as a drop target.
376 DirectoryItem.prototype.doDropTargetAction = function() {
377 this.expanded = true;
381 * Sets the context menu for directory tree.
382 * @param {cr.ui.Menu} menu Menu to be set.
384 DirectoryItem.prototype.setContextMenu = function(menu) {
385 var tree = this.parentTree_ || this; // If no parent, 'this' itself is tree.
386 var locationInfo = tree.volumeManager_.getLocationInfo(this.entry);
387 if (locationInfo && locationInfo.isEligibleForFolderShortcut)
388 cr.ui.contextMenuHandler.setContextMenu(this, menu);
391 ////////////////////////////////////////////////////////////////////////////////
395 * Tree of directories on the middle bar. This element is also the root of
396 * items, in other words, this is the parent of the top-level items.
399 * @extends {cr.ui.Tree}
401 function DirectoryTree() {}
404 * Decorates an element.
405 * @param {HTMLElement} el Element to be DirectoryTree.
406 * @param {DirectoryModel} directoryModel Current DirectoryModel.
407 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
409 DirectoryTree.decorate = function(el, directoryModel, volumeManager) {
410 el.__proto__ = DirectoryTree.prototype;
411 (/** @type {DirectoryTree} */ el).decorate(directoryModel, volumeManager);
414 DirectoryTree.prototype = {
415 __proto__: cr.ui.Tree.prototype,
417 // DirectoryTree is always expanded.
418 get expanded() { return true; },
420 * @param {boolean} value Not used.
422 set expanded(value) {},
425 * The DirectoryEntry corresponding to this DirectoryItem. This may be
426 * a dummy DirectoryEntry.
427 * @type {DirectoryEntry|Object}
431 return this.dirEntry_;
435 * The DirectoryModel this tree corresponds to.
436 * @type {DirectoryModel}
438 get directoryModel() {
439 return this.directoryModel_;
443 * The VolumeManager instance of the system.
444 * @type {VolumeManager}
446 get volumeManager() {
447 return this.volumeManager_;
451 cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
454 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
456 * @param {boolean} recursive True if the all visible sub-directories are
457 * updated recursively including left arrows. If false, the update walks
458 * only immediate child directories without arrows.
460 DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
461 DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
465 * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
467 * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
469 * @return {boolean} True if the parent item is found.
471 DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
472 return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
476 * Decorates an element.
477 * @param {DirectoryModel} directoryModel Current DirectoryModel.
478 * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
480 DirectoryTree.prototype.decorate = function(directoryModel, volumeManager) {
481 cr.ui.Tree.prototype.decorate.call(this);
484 this.directoryModel_ = directoryModel;
485 this.volumeManager_ = volumeManager;
487 this.currentVolumeInfo_ = null;
489 this.fileFilter_ = this.directoryModel_.getFileFilter();
490 this.fileFilter_.addEventListener('changed',
491 this.onFilterChanged_.bind(this));
493 this.directoryModel_.addEventListener('directory-changed',
494 this.onCurrentDirectoryChanged_.bind(this));
496 this.privateOnDirectoryChangedBound_ =
497 this.onDirectoryContentChanged_.bind(this);
498 chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
499 this.privateOnDirectoryChangedBound_);
501 this.scrollBar_ = MainPanelScrollBar();
502 this.scrollBar_.initialize(this.parentNode, this);
506 * Select the item corresponding to the given entry.
507 * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
510 DirectoryTree.prototype.selectByEntry = function(entry) {
511 // If the target directory is not in the tree, do nothing.
512 var locationInfo = this.volumeManager_.getLocationInfo(entry);
513 if (!locationInfo || !locationInfo.isDriveBased)
516 var volumeInfo = this.volumeManager_.getVolumeInfo(entry);
517 if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
520 if (this.searchAndSelectByEntry(entry))
523 this.updateSubDirectories(false /* recursive */);
524 var currentSequence = ++this.sequence_;
525 volumeInfo.resolveDisplayRoot(function() {
526 if (this.sequence_ !== currentSequence)
528 if (!this.searchAndSelectByEntry(entry))
529 this.selectedItem = null;
534 * Retrieves the latest subdirectories and update them on the tree.
536 * @param {boolean} recursive True if the update is recursively.
537 * @param {function()=} opt_callback Called when subdirectories are fully
540 DirectoryTree.prototype.updateSubDirectories = function(
541 recursive, opt_callback) {
542 var callback = opt_callback || function() {};
545 var compareEntries = function(a, b) {
546 return a.toURL() < b.toURL();
549 // Add fakes (if any).
550 for (var key in this.currentVolumeInfo_.fakeEntries) {
551 this.entries_.push(this.currentVolumeInfo_.fakeEntries[key]);
554 // If the display root is not available yet, then redraw anyway with what
555 // we have. However, concurrently try to resolve the display root and then
557 if (!this.currentVolumeInfo_.displayRoot) {
558 this.entries_.sort(compareEntries);
559 this.redraw(recursive);
562 this.currentVolumeInfo_.resolveDisplayRoot(function(displayRoot) {
563 this.entries_.push(this.currentVolumeInfo_.displayRoot);
564 this.entries_.sort(compareEntries);
565 this.redraw(recursive); // Redraw.
567 }.bind(this), callback /* Ignore errors. */);
572 * @param {boolean} recursive True if the update is recursively. False if the
573 * only root items are updated.
575 DirectoryTree.prototype.redraw = function(recursive) {
576 this.updateSubElementsFromList(recursive);
580 * Invoked when the filter is changed.
583 DirectoryTree.prototype.onFilterChanged_ = function() {
584 // Returns immediately, if the tree is hidden.
588 this.redraw(true /* recursive */);
592 * Invoked when a directory is changed.
593 * @param {!UIEvent} event Event.
596 DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
597 if (event.eventType !== 'changed')
600 var locationInfo = this.volumeManager_.getLocationInfo(event.entry);
601 if (!locationInfo || !locationInfo.isDriveBased)
604 var myDriveItem = this.items[0];
606 myDriveItem.updateItemByEntry(event.entry);
610 * Invoked when the current directory is changed.
611 * @param {!UIEvent} event Event.
614 DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
615 this.currentVolumeInfo_ =
616 this.volumeManager_.getVolumeInfo(event.newDirEntry);
617 this.selectByEntry(event.newDirEntry);
621 * Sets the margin height for the transparent preview panel at the bottom.
622 * @param {number} margin Margin to be set in px.
624 DirectoryTree.prototype.setBottomMarginForPanel = function(margin) {
625 this.style.paddingBottom = margin + 'px';
626 this.scrollBar_.setBottomMarginForPanel(margin);
630 * Updates the UI after the layout has changed.
632 DirectoryTree.prototype.relayout = function() {
633 cr.dispatchSimpleEvent(this, 'relayout');