- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / foreground / js / directory_tree.js
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.
4
5 'use strict';
6
7 ////////////////////////////////////////////////////////////////////////////////
8 // DirectoryTreeUtil
9
10 /**
11  * Utility methods. They are intended for use only in this file.
12  */
13 var DirectoryTreeUtil = {};
14
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   ];
26
27   for (var i = 0; i < entries.length; i++) {
28     entries[i]['label'] = PathUtil.getRootLabel(entries[i].fullPath);
29   }
30
31   return entries;
32 };
33
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);
43 };
44
45 Object.freeze(DirectoryTreeUtil);
46
47 ////////////////////////////////////////////////////////////////////////////////
48 // DirectoryTreeBase
49
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 = {};
57
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];
72
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 */);
80
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   }
90
91   var removedChild;
92   while (removedChild = this.items[index]) {
93     this.remove(removedChild);
94   }
95
96   if (index == 0) {
97     this.hasChildren = false;
98     this.expanded = false;
99   } else {
100     this.hasChildren = true;
101   }
102 };
103
104 /**
105  * Finds a parent directory of the {@code entry} in {@code this}, and
106  * invokes the DirectoryItem.selectByEntry() of the found directory.
107  *
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;
121 };
122
123 Object.freeze(DirectoryItemTreeBaseMethods);
124
125 ////////////////////////////////////////////////////////////////////////////////
126 // DirectoryItem
127
128 /**
129  * A directory in the tree. Each element represents one directory.
130  *
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;
141 }
142
143 /**
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);
154 };
155
156 DirectoryItem.prototype = {
157   __proto__: cr.ui.TreeItem.prototype,
158
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   },
167
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   }
176 };
177
178 /**
179  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
180  *
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);
187 };
188
189 /**
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);
197 };
198
199 /**
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;
209
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 class="root-eject"></div>' +
217       '</div>' +
218       '<div class="tree-children"></div>';
219   this.setAttribute('role', 'treeitem');
220
221   this.parentTree_ = tree;
222   this.directoryModel_ = tree.directoryModel;
223   this.parent_ = parentDirItem;
224   this.label = label;
225   this.fullPath = path;
226   this.dirEntry_ = dirEntry;
227   this.fileFilter_ = this.directoryModel_.getFileFilter();
228
229   // Sets hasChildren=false tentatively. This will be overridden after
230   // scanning sub-directories in DirectoryTreeUtil.updateSubElementsFromList.
231   this.hasChildren = false;
232
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);
239   else
240     icon.setAttribute('file-type-icon', 'folder');
241
242   var eject = this.querySelector('.root-eject');
243   eject.hidden = !PathUtil.isUnmountableByUser(path);
244   eject.addEventListener('click',
245       function(event) {
246         event.stopPropagation();
247         if (!PathUtil.isUnmountableByUser(path))
248           return;
249
250         tree.volumeManager.unmount(path, function() {}, function() {});
251       }.bind(this));
252
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));
259
260   if (parentDirItem.expanded)
261     this.updateSubDirectories(false /* recursive */);
262 };
263
264 /**
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.
267  *
268  * @param {boolean} unused Unused.
269  * @override
270  */
271 DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
272 };
273
274 /**
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.
278  *
279  * @param {!cr.ui.TreeItem} child The tree item child to remove.
280  * @override
281  */
282 DirectoryItem.prototype.remove = function(child) {
283   this.lastElementChild.removeChild(child);
284   if (this.items.length == 0)
285     this.hasChildren = false;
286 };
287
288 /**
289  * Invoked when the item is being expanded.
290  * @param {!UIEvent} e Event.
291  * @private
292  **/
293 DirectoryItem.prototype.onExpand_ = function(e) {
294   this.updateSubDirectories(
295       true /* recursive */,
296       function() {},
297       function() {
298         this.expanded = false;
299       }.bind(this));
300
301   e.stopPropagation();
302 };
303
304 /**
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.
309  */
310 DirectoryItem.prototype.updateSubDirectories = function(
311     recursive, opt_successCallback, opt_errorCallback) {
312   if (util.isFakeDirectoryEntry(this.entry)) {
313     if (opt_errorCallback)
314       opt_errorCallback();
315     return;
316   }
317
318   var sortEntries = function(fileFilter, entries) {
319     entries.sort(function(a, b) {
320       return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1;
321     });
322     return entries.filter(fileFilter.filter.bind(fileFilter));
323   };
324
325   var onSuccess = function(entries) {
326     this.entries_ = entries;
327     this.redrawSubDirectoryList_(recursive);
328     opt_successCallback && opt_successCallback();
329   }.bind(this);
330
331   var reader = this.entry.createReader();
332   var entries = [];
333   var readEntry = function() {
334     reader.readEntries(function(results) {
335       if (!results.length) {
336         onSuccess(sortEntries(this.fileFilter_, entries));
337         return;
338       }
339
340       for (var i = 0; i < results.length; i++) {
341         var entry = results[i];
342         if (entry.isDirectory)
343           entries.push(entry);
344       }
345       readEntry();
346     }.bind(this));
347   }.bind(this);
348   readEntry();
349 };
350
351 /**
352  * Updates sub-elements of {@code parentElement} reading {@code DirectoryEntry}
353  * with calling {@code iterator}.
354  *
355  * @param {string} changedDirectryPath The path of the changed directory.
356  */
357 DirectoryItem.prototype.updateItemByPath = function(changedDirectryPath) {
358   if (changedDirectryPath === this.entry.fullPath) {
359     this.updateSubDirectories(false /* recursive */);
360     return;
361   }
362
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);
367       break;
368     }
369   }
370 };
371
372 /**
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.
376  * @private
377  */
378 DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
379   this.updateSubElementsFromList(recursive);
380 };
381
382 /**
383  * Select the item corresponding to the given {@code entry}.
384  * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
385  */
386 DirectoryItem.prototype.selectByEntry = function(entry) {
387   if (util.isSameEntry(entry, this.entry)) {
388     this.selected = true;
389     return;
390   }
391
392   if (this.searchAndSelectByEntry(entry))
393     return;
394
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));
399 };
400
401 /**
402  * Executes the assigned action as a drop target.
403  */
404 DirectoryItem.prototype.doDropTargetAction = function() {
405   this.expanded = true;
406 };
407
408 /**
409  * Executes the assigned action. DirectoryItem performs changeDirectory.
410  */
411 DirectoryItem.prototype.doAction = function() {
412   if (this.fullPath != this.directoryModel_.getCurrentDirPath())
413     this.directoryModel_.changeDirectory(this.fullPath);
414 };
415
416 /**
417  * Sets the context menu for directory tree.
418  * @param {cr.ui.Menu} menu Menu to be set.
419  */
420 DirectoryItem.prototype.setContextMenu = function(menu) {
421   if (this.entry && PathUtil.isEligibleForFolderShortcut(this.entry.fullPath))
422     cr.ui.contextMenuHandler.setContextMenu(this, menu);
423 };
424
425 ////////////////////////////////////////////////////////////////////////////////
426 // DirectoryTree
427
428 /**
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.
431  *
432  * @constructor
433  * @extends {cr.ui.Tree}
434  */
435 function DirectoryTree() {}
436
437 /**
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.
442  */
443 DirectoryTree.decorate = function(el, directoryModel, volumeManager) {
444   el.__proto__ = DirectoryTree.prototype;
445   (/** @type {DirectoryTree} */ el).decorate(directoryModel, volumeManager);
446 };
447
448 DirectoryTree.prototype = {
449   __proto__: cr.ui.Tree.prototype,
450
451   // DirectoryTree is always expanded.
452   get expanded() { return true; },
453   /**
454    * @param {boolean} value Not used.
455    */
456   set expanded(value) {},
457
458   /**
459    * The DirectoryEntry corresponding to this DirectoryItem. This may be
460    * a dummy DirectoryEntry.
461    * @type {DirectoryEntry|Object}
462    * @override
463    **/
464   get entry() {
465     return this.dirEntry_;
466   },
467
468   /**
469    * The DirectoryModel this tree corresponds to.
470    * @type {DirectoryModel}
471    */
472   get directoryModel() {
473     return this.directoryModel_;
474   },
475
476   /**
477    * The VolumeManager instance of the system.
478    * @type {VolumeManager}
479    */
480   get volumeManager() {
481     return this.volumeManager_;
482   },
483 };
484
485 cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
486
487 /**
488  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
489  *
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.
493  */
494 DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
495   DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
496 };
497
498 /**
499  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
500  * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
501  *     a fake.
502  * @return {boolean} True if the parent item is found.
503  */
504 DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
505   return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
506 };
507
508 /**
509  * Decorates an element.
510  * @param {DirectoryModel} directoryModel Current DirectoryModel.
511  * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
512  */
513 DirectoryTree.prototype.decorate = function(directoryModel, volumeManager) {
514   cr.ui.Tree.prototype.decorate.call(this);
515
516   this.directoryModel_ = directoryModel;
517   this.volumeManager_ = volumeManager;
518   this.entries_ = DirectoryTreeUtil.generateTopLevelEntries();
519
520   this.fileFilter_ = this.directoryModel_.getFileFilter();
521   this.fileFilter_.addEventListener('changed',
522                                     this.onFilterChanged_.bind(this));
523
524   this.directoryModel_.addEventListener('directory-changed',
525       this.onCurrentDirectoryChanged_.bind(this));
526
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();
534       return;
535     }
536   }.bind(this));
537
538   this.privateOnDirectoryChangedBound_ =
539       this.onDirectoryContentChanged_.bind(this);
540   chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
541       this.privateOnDirectoryChangedBound_);
542
543   this.scrollBar_ = MainPanelScrollBar();
544   this.scrollBar_.initialize(this.parentNode, this);
545
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 */);
552   }.bind(this));
553 };
554
555 /**
556  * Select the item corresponding to the given entry.
557  * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
558  *     be a fake.
559  */
560 DirectoryTree.prototype.selectByEntry = function(entry) {
561   // If the target directory is not in the tree, do nothing.
562   if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath))
563     return;
564
565   this.maybeResolveMyDriveRoot_(function() {
566     if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
567       return;
568
569     if (this.searchAndSelectByEntry(entry))
570       return;
571
572     this.selectedItem = null;
573     this.updateSubDirectories(
574         false /* recursive */,
575         // Success callback, failure is not handled.
576         function() {
577           if (!this.searchAndSelectByEntry(entry))
578             this.selectedItem = null;
579         }.bind(this));
580   }.bind(this));
581 };
582
583 /**
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
586  * immediately.
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.
590  * @private
591  */
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();
598     return;
599   }
600
601   // The entry is a fake.
602   this.directoryModel_.resolveDirectory(
603       myDriveItem.fullPath,
604       function(entry) {
605         if (!util.isFakeDirectoryEntry(entry)) {
606           myDriveItem.dirEntry_ = entry;
607         }
608
609         completionCallback();
610       },
611       completionCallback);
612 };
613
614 /**
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.
619  */
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();
626 };
627
628 /**
629  * Redraw the list.
630  * @param {boolean} recursive True if the update is recursively. False if the
631  *     only root items are updated.
632  */
633 DirectoryTree.prototype.redraw = function(recursive) {
634   this.updateSubElementsFromList(recursive);
635 };
636
637 /**
638  * Invoked when the filter is changed.
639  * @private
640  */
641 DirectoryTree.prototype.onFilterChanged_ = function() {
642   // Returns immediately, if the tree is hidden.
643   if (this.hidden)
644     return;
645
646   this.redraw(true /* recursive */);
647 };
648
649 /**
650  * Invoked when a directory is changed.
651  * @param {!UIEvent} event Event.
652  * @private
653  */
654 DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
655   if (event.eventType == 'changed') {
656     var path = util.extractFilePath(event.directoryUrl);
657     if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(path))
658       return;
659
660     var myDriveItem = this.items[0];
661     myDriveItem.updateItemByPath(path);
662   }
663 };
664
665 /**
666  * Invoked when the current directory is changed.
667  * @param {!UIEvent} event Event.
668  * @private
669  */
670 DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
671   this.selectByEntry(event.newDirEntry);
672 };
673
674 /**
675  * Sets the margin height for the transparent preview panel at the bottom.
676  * @param {number} margin Margin to be set in px.
677  */
678 DirectoryTree.prototype.setBottomMarginForPanel = function(margin) {
679   this.style.paddingBottom = margin + 'px';
680   this.scrollBar_.setBottomMarginForPanel(margin);
681 };
682
683 /**
684  * Updates the UI after the layout has changed.
685  */
686 DirectoryTree.prototype.relayout = function() {
687   cr.dispatchSimpleEvent(this, 'relayout');
688 };