cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / foreground / js / directory_tree.js
blob5c9de7aa0fedd429f039e18ceea0e2397bc59532
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.
5 'use strict';
7 ////////////////////////////////////////////////////////////////////////////////
8 // DirectoryTreeUtil
10 /**
11  * Utility methods. They are intended for use only in this file.
12  */
13 var DirectoryTreeUtil = {};
15 /**
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.
18  */
19 DirectoryTreeUtil.generateTopLevelEntries = function() {
20   var entries = [
21     DirectoryModel.fakeDriveEntry_,
22     DirectoryModel.fakeDriveOfflineEntry_,
23     DirectoryModel.fakeDriveSharedWithMeEntry_,
24     DirectoryModel.fakeDriveRecentEntry_,
25   ];
27   for (var i = 0; i < entries.length; i++) {
28     entries[i]['label'] = PathUtil.getRootLabel(entries[i].fullPath);
29   }
31   return entries;
34 /**
35  * Checks if the given directory can be on the tree or not.
36  *
37  * @param {string} path Path to be checked.
38  * @return {boolean} True if the path is eligible for the directory tree.
39  *     Otherwise, false.
40  */
41 DirectoryTreeUtil.isEligiblePathForDirectoryTree = function(path) {
42   return PathUtil.isDriveBasedPath(path);
45 Object.freeze(DirectoryTreeUtil);
47 ////////////////////////////////////////////////////////////////////////////////
48 // DirectoryTreeBase
50 /**
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.
55  */
56 var DirectoryItemTreeBaseMethods = {};
58 /**
59  * Updates sub-elements of {@code this} reading {@code DirectoryEntry}.
60  * The list of {@code DirectoryEntry} are not updated by this method.
61  *
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.
65  */
66 DirectoryItemTreeBaseMethods.updateSubElementsFromList = function(recursive) {
67   var index = 0;
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);
75       this.add(item);
76       index++;
77     } else if (currentEntry.fullPath == currentElement.fullPath) {
78       if (recursive && this.expanded)
79         currentElement.updateSubDirectories(true /* recursive */);
81       index++;
82     } else if (currentEntry.fullPath < currentElement.fullPath) {
83       var item = new DirectoryItem(currentEntry, this, tree);
84       this.addAt(item, index);
85       index++;
86     } else if (currentEntry.fullPath > currentElement.fullPath) {
87       this.remove(currentElement);
88     }
89   }
91   var removedChild;
92   while (removedChild = this.items[index]) {
93     this.remove(removedChild);
94   }
96   if (index == 0) {
97     this.hasChildren = false;
98     this.expanded = false;
99   } else {
100     this.hasChildren = true;
101   }
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
109  *     a fake.
110  * @return {boolean} True if the parent item is found.
111  */
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);
117       return true;
118     }
119   }
120   return false;
123 Object.freeze(DirectoryItemTreeBaseMethods);
125 ////////////////////////////////////////////////////////////////////////////////
126 // DirectoryItem
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}
135  * @constructor
136  */
137 function DirectoryItem(dirEntry, parentDirItem, tree) {
138   var item = cr.doc.createElement('div');
139   DirectoryItem.decorate(item, dirEntry, parentDirItem, tree);
140   return item;
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.
148  */
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,
159   /**
160    * The DirectoryEntry corresponding to this DirectoryItem. This may be
161    * a dummy DirectoryEntry.
162    * @type {DirectoryEntry|Object}
163    */
164   get entry() {
165     return this.dirEntry_;
166   },
168   /**
169    * The element containing the label text and the icon.
170    * @type {!HTMLElement}
171    * @override
172    */
173   get labelElement() {
174     return this.firstElementChild.querySelector('.label');
175   }
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.
184  */
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
192  *     a fake.
193  * @return {boolean} True if the parent item is found.
194  */
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.
203  */
204 DirectoryItem.prototype.decorate = function(
205     dirEntry, parentDirItem, tree) {
206   var path = dirEntry.fullPath;
207   var label;
208   label = dirEntry.label ? dirEntry.label : dirEntry.name;
210   this.className = 'tree-item';
211   this.innerHTML =
212       '<div class="tree-row">' +
213       ' <span class="expand-icon"></span>' +
214       ' <span class="icon"></span>' +
215       ' <span class="label"></span>' +
216       '</div>' +
217       '<div class="tree-children"></div>';
218   this.setAttribute('role', 'treeitem');
220   this.parentTree_ = tree;
221   this.directoryModel_ = tree.directoryModel;
222   this.parent_ = parentDirItem;
223   this.label = label;
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);
238   else
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.
257  * @override
258  */
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.
268  * @override
269  */
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.
279  * @private
280  **/
281 DirectoryItem.prototype.onExpand_ = function(e) {
282   this.updateSubDirectories(
283       true /* recursive */,
284       function() {},
285       function() {
286         this.expanded = false;
287       }.bind(this));
289   e.stopPropagation();
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.
297  */
298 DirectoryItem.prototype.updateSubDirectories = function(
299     recursive, opt_successCallback, opt_errorCallback) {
300   if (util.isFakeEntry(this.entry)) {
301     if (opt_errorCallback)
302       opt_errorCallback();
303     return;
304   }
306   var sortEntries = function(fileFilter, entries) {
307     entries.sort(function(a, b) {
308       return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1;
309     });
310     return entries.filter(fileFilter.filter.bind(fileFilter));
311   };
313   var onSuccess = function(entries) {
314     this.entries_ = entries;
315     this.redrawSubDirectoryList_(recursive);
316     opt_successCallback && opt_successCallback();
317   }.bind(this);
319   var reader = this.entry.createReader();
320   var entries = [];
321   var readEntry = function() {
322     reader.readEntries(function(results) {
323       if (!results.length) {
324         onSuccess(sortEntries(this.fileFilter_, entries));
325         return;
326       }
328       for (var i = 0; i < results.length; i++) {
329         var entry = results[i];
330         if (entry.isDirectory)
331           entries.push(entry);
332       }
333       readEntry();
334     }.bind(this));
335   }.bind(this);
336   readEntry();
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.
344  */
345 DirectoryItem.prototype.updateItemByPath = function(changedDirectryPath) {
346   if (changedDirectryPath === this.entry.fullPath) {
347     this.updateSubDirectories(false /* recursive */);
348     return;
349   }
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);
355       break;
356     }
357   }
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.
364  * @private
365  */
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.
373  */
374 DirectoryItem.prototype.selectByEntry = function(entry) {
375   if (util.isSameEntry(entry, this.entry)) {
376     this.selected = true;
377     return;
378   }
380   if (this.searchAndSelectByEntry(entry))
381     return;
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.
391  */
392 DirectoryItem.prototype.doDropTargetAction = function() {
393   this.expanded = true;
397  * Executes the assigned action. DirectoryItem performs changeDirectory.
398  */
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.
407  */
408 DirectoryItem.prototype.setContextMenu = function(menu) {
409   if (this.entry && PathUtil.isEligibleForFolderShortcut(this.entry.fullPath))
410     cr.ui.contextMenuHandler.setContextMenu(this, menu);
413 ////////////////////////////////////////////////////////////////////////////////
414 // DirectoryTree
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.
420  * @constructor
421  * @extends {cr.ui.Tree}
422  */
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.
430  */
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; },
441   /**
442    * @param {boolean} value Not used.
443    */
444   set expanded(value) {},
446   /**
447    * The DirectoryEntry corresponding to this DirectoryItem. This may be
448    * a dummy DirectoryEntry.
449    * @type {DirectoryEntry|Object}
450    * @override
451    **/
452   get entry() {
453     return this.dirEntry_;
454   },
456   /**
457    * The DirectoryModel this tree corresponds to.
458    * @type {DirectoryModel}
459    */
460   get directoryModel() {
461     return this.directoryModel_;
462   },
464   /**
465    * The VolumeManager instance of the system.
466    * @type {VolumeManager}
467    */
468   get volumeManager() {
469     return this.volumeManager_;
470   },
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.
481  */
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
489  *     a fake.
490  * @return {boolean} True if the parent item is found.
491  */
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.
500  */
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();
522       return;
523     }
524   }.bind(this));
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 */);
540   }.bind(this));
544  * Select the item corresponding to the given entry.
545  * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
546  *     be a fake.
547  */
548 DirectoryTree.prototype.selectByEntry = function(entry) {
549   // If the target directory is not in the tree, do nothing.
550   if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath))
551     return;
553   this.maybeResolveMyDriveRoot_(function() {
554     if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
555       return;
557     if (this.searchAndSelectByEntry(entry))
558       return;
560     this.selectedItem = null;
561     this.updateSubDirectories(
562         false /* recursive */,
563         // Success callback, failure is not handled.
564         function() {
565           if (!this.searchAndSelectByEntry(entry))
566             this.selectedItem = null;
567         }.bind(this));
568   }.bind(this));
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
574  * immediately.
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.
578  * @private
579  */
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();
586     return;
587   }
589   // The entry is a fake.
590   this.directoryModel_.resolveDirectory(
591       myDriveItem.fullPath,
592       function(entry) {
593         if (!util.isFakeEntry(entry))
594           myDriveItem.dirEntry_ = entry;
596         completionCallback();
597       },
598       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.
606  */
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();
616  * Redraw the list.
617  * @param {boolean} recursive True if the update is recursively. False if the
618  *     only root items are updated.
619  */
620 DirectoryTree.prototype.redraw = function(recursive) {
621   this.updateSubElementsFromList(recursive);
625  * Invoked when the filter is changed.
626  * @private
627  */
628 DirectoryTree.prototype.onFilterChanged_ = function() {
629   // Returns immediately, if the tree is hidden.
630   if (this.hidden)
631     return;
633   this.redraw(true /* recursive */);
637  * Invoked when a directory is changed.
638  * @param {!UIEvent} event Event.
639  * @private
640  */
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))
646       return;
648     var myDriveItem = this.items[0];
649     myDriveItem.updateItemByPath(event.entry.fullPath);
650   }
654  * Invoked when the current directory is changed.
655  * @param {!UIEvent} event Event.
656  * @private
657  */
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.
665  */
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.
673  */
674 DirectoryTree.prototype.relayout = function() {
675   cr.dispatchSimpleEvent(this, 'relayout');