Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / 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 // DirectoryTreeBase
9
10 /**
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.
15  */
16 var DirectoryItemTreeBaseMethods = {};
17
18 /**
19  * Updates sub-elements of {@code this} reading {@code DirectoryEntry}.
20  * The list of {@code DirectoryEntry} are not updated by this method.
21  *
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.
25  */
26 DirectoryItemTreeBaseMethods.updateSubElementsFromList = function(recursive) {
27   var index = 0;
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);
33
34     if (index >= this.items.length) {
35       var item = new DirectoryItem(label, currentEntry, this, tree);
36       this.add(item);
37       index++;
38     } else if (util.isSameEntry(currentEntry, currentElement.entry)) {
39       currentElement.updateSharedStatusIcon();
40       if (recursive && this.expanded)
41         currentElement.updateSubDirectories(true /* recursive */);
42
43       index++;
44     } else if (currentEntry.toURL() < currentElement.entry.toURL()) {
45       var item = new DirectoryItem(label, currentEntry, this, tree);
46       this.addAt(item, index);
47       index++;
48     } else if (currentEntry.toURL() > currentElement.entry.toURL()) {
49       this.remove(currentElement);
50     }
51   }
52
53   var removedChild;
54   while (removedChild = this.items[index]) {
55     this.remove(removedChild);
56   }
57
58   if (index === 0) {
59     this.hasChildren = false;
60     this.expanded = false;
61   } else {
62     this.hasChildren = true;
63   }
64 };
65
66 /**
67  * Finds a parent directory of the {@code entry} in {@code this}, and
68  * invokes the DirectoryItem.selectByEntry() of the found directory.
69  *
70  * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
71  *     a fake.
72  * @return {boolean} True if the parent item is found.
73  */
74 DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
75   for (var i = 0; i < this.items.length; i++) {
76     var item = this.items[i];
77     if (util.isDescendantEntry(item.entry, entry) ||
78         util.isSameEntry(item.entry, entry)) {
79       item.selectByEntry(entry);
80       return true;
81     }
82   }
83   return false;
84 };
85
86 Object.freeze(DirectoryItemTreeBaseMethods);
87
88 var TREE_ITEM_INNTER_HTML =
89     '<div class="tree-row">' +
90     ' <span class="expand-icon"></span>' +
91     ' <span class="icon"></span>' +
92     ' <span class="label entry-name"></span>' +
93     '</div>' +
94     '<div class="tree-children"></div>';
95
96 ////////////////////////////////////////////////////////////////////////////////
97 // DirectoryItem
98
99 /**
100  * A directory in the tree. Each element represents one directory.
101  *
102  * @param {string} label Label for this item.
103  * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
104  * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
105  * @param {DirectoryTree} tree Current tree, which contains this item.
106  * @extends {cr.ui.TreeItem}
107  * @constructor
108  */
109 function DirectoryItem(label, dirEntry, parentDirItem, tree) {
110   var item = new cr.ui.TreeItem();
111   DirectoryItem.decorate(item, label, dirEntry, parentDirItem, tree);
112   return item;
113 }
114
115 /**
116  * @param {HTMLElement} el Element to be DirectoryItem.
117  * @param {string} label Label for this item.
118  * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
119  * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
120  * @param {DirectoryTree} tree Current tree, which contains this item.
121  */
122 DirectoryItem.decorate =
123     function(el, label, dirEntry, parentDirItem, tree) {
124   el.__proto__ = DirectoryItem.prototype;
125   (/** @type {DirectoryItem} */ el).decorate(
126       label, dirEntry, parentDirItem, tree);
127 };
128
129 DirectoryItem.prototype = {
130   __proto__: cr.ui.TreeItem.prototype,
131
132   /**
133    * The DirectoryEntry corresponding to this DirectoryItem. This may be
134    * a dummy DirectoryEntry.
135    * @type {DirectoryEntry|Object}
136    */
137   get entry() {
138     return this.dirEntry_;
139   },
140
141   /**
142    * The element containing the label text and the icon.
143    * @type {!HTMLElement}
144    * @override
145    */
146   get labelElement() {
147     return this.firstElementChild.querySelector('.label');
148   }
149 };
150
151 /**
152  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
153  *
154  * @param {boolean} recursive True if the all visible sub-directories are
155  *     updated recursively including left arrows. If false, the update walks
156  *     only immediate child directories without arrows.
157  */
158 DirectoryItem.prototype.updateSubElementsFromList = function(recursive) {
159   DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
160 };
161
162 /**
163  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
164  *
165  * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
166  *     a fake.
167  * @return {boolean} True if the parent item is found.
168  */
169 DirectoryItem.prototype.searchAndSelectByEntry = function(entry) {
170   return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
171 };
172
173 /**
174  * @param {string} label Localized label for this item.
175  * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
176  * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
177  * @param {DirectoryTree} tree Current tree, which contains this item.
178  */
179 DirectoryItem.prototype.decorate = function(
180     label, dirEntry, parentDirItem, tree) {
181   this.innerHTML = TREE_ITEM_INNTER_HTML;
182   this.parentTree_ = tree;
183   this.directoryModel_ = tree.directoryModel;
184   this.parent_ = parentDirItem;
185   this.label = label;
186   this.dirEntry_ = dirEntry;
187   this.fileFilter_ = this.directoryModel_.getFileFilter();
188
189   // Sets hasChildren=false tentatively. This will be overridden after
190   // scanning sub-directories in updateSubElementsFromList().
191   this.hasChildren = false;
192
193   this.addEventListener('expand', this.onExpand_.bind(this), false);
194   var icon = this.querySelector('.icon');
195   icon.classList.add('volume-icon');
196   var location = tree.volumeManager.getLocationInfo(dirEntry);
197   if (location && location.rootType && location.isRootEntry) {
198     icon.setAttribute('volume-type-icon', location.rootType);
199   } else {
200     icon.setAttribute('file-type-icon', 'folder');
201     this.updateSharedStatusIcon();
202   }
203
204   if (this.parentTree_.contextMenuForSubitems)
205     this.setContextMenu(this.parentTree_.contextMenuForSubitems);
206   // Adds handler for future change.
207   this.parentTree_.addEventListener(
208       'contextMenuForSubitemsChange',
209       function(e) { this.setContextMenu(e.newValue); }.bind(this));
210
211   if (parentDirItem.expanded)
212     this.updateSubDirectories(false /* recursive */);
213 };
214
215 /**
216  * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
217  * a complex layout. This call is not necessary, so we are ignoring it.
218  *
219  * @param {boolean} unused Unused.
220  * @override
221  */
222 DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
223 };
224
225 /**
226  * Removes the child node, but without selecting the parent item, to avoid
227  * unintended changing of directories. Removing is done externally, and other
228  * code will navigate to another directory.
229  *
230  * @param {!cr.ui.TreeItem} child The tree item child to remove.
231  * @override
232  */
233 DirectoryItem.prototype.remove = function(child) {
234   this.lastElementChild.removeChild(child);
235   if (this.items.length == 0)
236     this.hasChildren = false;
237 };
238
239 /**
240  * Invoked when the item is being expanded.
241  * @param {!UIEvent} e Event.
242  * @private
243  **/
244 DirectoryItem.prototype.onExpand_ = function(e) {
245   this.updateSubDirectories(
246       true /* recursive */,
247       function() {},
248       function() {
249         this.expanded = false;
250       }.bind(this));
251
252   e.stopPropagation();
253 };
254
255 /**
256  * Invoked when the tree item is clicked.
257  *
258  * @param {Event} e Click event.
259  * @override
260  */
261 DirectoryItem.prototype.handleClick = function(e) {
262   cr.ui.TreeItem.prototype.handleClick.call(this, e);
263   if (!e.target.classList.contains('expand-icon'))
264     this.directoryModel_.activateDirectoryEntry(this.entry);
265 };
266
267 /**
268  * Retrieves the latest subdirectories and update them on the tree.
269  * @param {boolean} recursive True if the update is recursively.
270  * @param {function()=} opt_successCallback Callback called on success.
271  * @param {function()=} opt_errorCallback Callback called on error.
272  */
273 DirectoryItem.prototype.updateSubDirectories = function(
274     recursive, opt_successCallback, opt_errorCallback) {
275   if (util.isFakeEntry(this.entry)) {
276     if (opt_errorCallback)
277       opt_errorCallback();
278     return;
279   }
280
281   var sortEntries = function(fileFilter, entries) {
282     entries.sort(util.compareName);
283     return entries.filter(fileFilter.filter.bind(fileFilter));
284   };
285
286   var onSuccess = function(entries) {
287     this.entries_ = entries;
288     this.redrawSubDirectoryList_(recursive);
289     opt_successCallback && opt_successCallback();
290   }.bind(this);
291
292   var reader = this.entry.createReader();
293   var entries = [];
294   var readEntry = function() {
295     reader.readEntries(function(results) {
296       if (!results.length) {
297         onSuccess(sortEntries(this.fileFilter_, entries));
298         return;
299       }
300
301       for (var i = 0; i < results.length; i++) {
302         var entry = results[i];
303         if (entry.isDirectory)
304           entries.push(entry);
305       }
306       readEntry();
307     }.bind(this));
308   }.bind(this);
309   readEntry();
310 };
311
312 /**
313  * Searches for the changed directory in the current subtree, and if it is found
314  * then updates it.
315  *
316  * @param {DirectoryEntry} changedDirectoryEntry The entry ot the changed
317  *     directory.
318  */
319 DirectoryItem.prototype.updateItemByEntry = function(changedDirectoryEntry) {
320   if (util.isSameEntry(changedDirectoryEntry, this.entry)) {
321     this.updateSubDirectories(false /* recursive */);
322     return;
323   }
324
325   // Traverse the entire subtree to find the changed element.
326   for (var i = 0; i < this.items.length; i++) {
327     var item = this.items[i];
328     if (util.isDescendantEntry(item.entry, changedDirectoryEntry) ||
329         util.isSameEntry(item.entry, changedDirectoryEntry)) {
330       item.updateItemByEntry(changedDirectoryEntry);
331       break;
332     }
333   }
334 };
335
336 /**
337  * Update the icon based on whether the folder is shared on Drive.
338  */
339 DirectoryItem.prototype.updateSharedStatusIcon = function() {
340   var icon = this.querySelector('.icon');
341   this.parentTree_.metadataCache.getLatest(
342       [this.dirEntry_],
343       'external',
344       function(metadata) {
345         icon.classList.toggle('shared', metadata[0] && metadata[0].shared);
346       });
347 };
348
349 /**
350  * Redraw subitems with the latest information. The items are sorted in
351  * alphabetical order, case insensitive.
352  * @param {boolean} recursive True if the update is recursively.
353  * @private
354  */
355 DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
356   this.updateSubElementsFromList(recursive);
357 };
358
359 /**
360  * Select the item corresponding to the given {@code entry}.
361  * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
362  */
363 DirectoryItem.prototype.selectByEntry = function(entry) {
364   if (util.isSameEntry(entry, this.entry)) {
365     this.selected = true;
366     return;
367   }
368
369   if (this.searchAndSelectByEntry(entry))
370     return;
371
372   // If the entry doesn't exist, updates sub directories and tries again.
373   this.updateSubDirectories(
374       false /* recursive */,
375       this.searchAndSelectByEntry.bind(this, entry));
376 };
377
378 /**
379  * Executes the assigned action as a drop target.
380  */
381 DirectoryItem.prototype.doDropTargetAction = function() {
382   this.expanded = true;
383 };
384
385 /**
386  * Sets the context menu for directory tree.
387  * @param {cr.ui.Menu} menu Menu to be set.
388  */
389 DirectoryItem.prototype.setContextMenu = function(menu) {
390   var tree = this.parentTree_ || this;  // If no parent, 'this' itself is tree.
391   var locationInfo = tree.volumeManager_.getLocationInfo(this.entry);
392   if (locationInfo && locationInfo.isEligibleForFolderShortcut)
393     cr.ui.contextMenuHandler.setContextMenu(this, menu);
394 };
395
396 /**
397  * Change current directory to the entry of this item.
398  */
399 DirectoryItem.prototype.activate = function() {
400   this.parentTree_.directoryModel.activateDirectoryEntry(this.entry);
401 };
402
403 ////////////////////////////////////////////////////////////////////////////////
404 // VolumeItem
405
406 /**
407  * A TreeItem which represents a volume. Volume items are displayed as
408  * top-level children of DirectoryTree.
409  *
410  * @param {NavigationModelItem} modelItem NavigationModelItem of this volume.
411  * @param {DirectoryTree} tree Current tree, which contains this item.
412  * @extends {cr.ui.TreeItem}
413  * @constructor
414  */
415 function VolumeItem(modelItem, tree) {
416   var item = new cr.ui.TreeItem();
417   item.__proto__ = VolumeItem.prototype;
418   item.decorate(modelItem, tree);
419   return item;
420 }
421
422 VolumeItem.prototype = {
423   __proto__: cr.ui.TreeItem.prototype,
424   get entry() {
425     return this.volumeInfo_.displayRoot;
426   },
427   get modelItem() {
428     return this.modelItem_;
429   },
430   get volumeInfo() {
431     return this.volumeInfo_;
432   },
433   get labelElement() {
434     return this.firstElementChild.querySelector('.label');
435   },
436   // Overrides the property 'expanded' to prevent volume items from shrinking.
437   get expanded() {
438     return Object.getOwnPropertyDescriptor(
439         cr.ui.TreeItem.prototype, 'expanded').get.call(this);
440   },
441   set expanded(b) {
442     if (!b)
443       return;
444     Object.getOwnPropertyDescriptor(
445         cr.ui.TreeItem.prototype, 'expanded').set.call(this, b);
446   }
447 };
448
449 /**
450  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
451  *
452  * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
453  *     a fake.
454  * @return {boolean} True if the parent item is found.
455  */
456 VolumeItem.prototype.searchAndSelectByEntry = function(entry) {
457   return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
458 };
459
460 /**
461  * Decorates this element.
462  * @param {NavigationModelItem} modelItem NavigationModelItem of this volume.
463  * @param {DirectoryTree} tree Current tree, which contains this item.
464  */
465 VolumeItem.prototype.decorate = function(modelItem, tree) {
466   this.innerHTML = TREE_ITEM_INNTER_HTML;
467   this.parentTree_ = tree;
468   this.modelItem_ = modelItem;
469   this.volumeInfo_ = modelItem.volumeInfo;
470   this.label = modelItem.volumeInfo.label;
471
472   this.setupIcon_(this.querySelector('.icon'), this.volumeInfo);
473   this.setupEjectButton_(this.rowElement);
474   if (tree.contextMenuForRootItems)
475     this.setContextMenu(tree.contextMenuForRootItems);
476
477   // Populate children of this volume using resolved display root.
478   this.volumeInfo_.resolveDisplayRoot(function(displayRoot) {
479     this.updateSubDirectories(false /* recursive */);
480   }.bind(this));
481 };
482
483 /**
484  * Invoked when the tree item is clicked.
485  *
486  * @param {Event} e Click event.
487  * @override
488  */
489 VolumeItem.prototype.handleClick = function(e) {
490   // If the currently selected volume is clicked, change current directory to
491   // the volume's root.
492   if (this.selected)
493     this.activate();
494
495   cr.ui.TreeItem.prototype.handleClick.call(this, e);
496
497   // Resets file selection when a volume is clicked.
498   this.parentTree_.directoryModel.clearSelection();
499
500   // If the Drive volume is clicked, select one of the children instead of this
501   // item itself.
502   if (this.isDrive()) {
503     this.volumeInfo_.resolveDisplayRoot(function(displayRoot) {
504       this.searchAndSelectByEntry(displayRoot);
505     }.bind(this));
506   }
507 };
508
509 /**
510  * Retrieves the latest subdirectories and update them on the tree.
511  * @param {boolean} recursive True if the update is recursively.
512  */
513 VolumeItem.prototype.updateSubDirectories = function(recursive) {
514   // Drive volume has children including fake entries (offline, recent, etc...).
515   if (this.isDrive() && this.entry && !this.hasChildren) {
516     var entries = [this.entry];
517     if (this.parentTree_.fakeEntriesVisible_) {
518       for (var key in this.volumeInfo.fakeEntries)
519         entries.push(this.volumeInfo.fakeEntries[key]);
520     }
521     // This list is sorted by URL on purpose.
522     entries.sort(function(a, b) { return a.toURL() < b.toURL(); });
523
524     for (var i = 0; i < entries.length; i++) {
525       var item = new DirectoryItem(
526           util.getEntryLabel(this.parentTree_.volumeManager_, entries[i]),
527           entries[i], this, this.parentTree_);
528       this.add(item);
529       item.updateSubDirectories(false);
530     }
531     this.expanded = true;
532   }
533 };
534
535 /**
536  * Searches for the changed directory in the current subtree, and if it is found
537  * then updates it.
538  *
539  * @param {DirectoryEntry} changedDirectoryEntry The entry ot the changed
540  *     directory.
541  */
542 VolumeItem.prototype.updateItemByEntry = function(changedDirectoryEntry) {
543   if (this.isDrive())
544     this.items[0].updateItemByEntry(changedDirectoryEntry);
545 };
546
547 /**
548  * Select the item corresponding to the given entry.
549  * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
550  *     be a fake.
551  */
552 VolumeItem.prototype.selectByEntry = function(entry) {
553   // If this volume is drive, find the item to be selected amang children.
554   if (this.isDrive()) {
555     this.searchAndSelectByEntry(entry);
556     return;
557   }
558   if (util.isSameEntry(this.entry, entry) ||
559       util.isDescendantEntry(this.entry, entry))
560     this.selected = true;
561 };
562
563 /**
564  * Sets the context menu for volume items.
565  * @param {cr.ui.Menu} menu Menu to be set.
566  */
567 VolumeItem.prototype.setContextMenu = function(menu) {
568   if (this.isRemovable_())
569     cr.ui.contextMenuHandler.setContextMenu(this, menu);
570 };
571
572 /**
573  * Change current entry to this volume's root directory.
574  */
575 VolumeItem.prototype.activate = function() {
576   var directoryModel = this.parentTree_.directoryModel;
577   var onEntryResolved = function(entry) {
578     // Changes directory to the model item's root directory if needed.
579     if (!util.isSameEntry(directoryModel.getCurrentDirEntry(), entry)) {
580       metrics.recordUserAction('FolderShortcut.Navigate');
581       directoryModel.changeDirectoryEntry(entry);
582     }
583     // In case of failure in resolveDisplayRoot() in the volume's decorate(),
584     // update the volume's children here.
585     this.updateSubDirectories(false);
586   }.bind(this);
587
588   this.volumeInfo.resolveDisplayRoot(
589       onEntryResolved,
590       function() {
591         // Error, the display root is not available. It may happen on Drive.
592         this.parentTree_.dataModel.onItemNotFoundError(this.modelItem);
593       }.bind(this));
594 };
595
596 /**
597  * @return {boolean} True if this is Drive volume.
598  */
599 VolumeItem.prototype.isDrive = function() {
600   return this.volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DRIVE;
601 };
602
603 /**
604  * @return {boolean} True if this volume can be removed by user operation.
605  * @private
606  */
607 VolumeItem.prototype.isRemovable_ = function() {
608   var volumeType = this.volumeInfo.volumeType;
609   return volumeType === VolumeManagerCommon.VolumeType.ARCHIVE ||
610          volumeType === VolumeManagerCommon.VolumeType.REMOVABLE ||
611          volumeType === VolumeManagerCommon.VolumeType.PROVIDED;
612 };
613
614 /**
615  * Set up icon of this volume item.
616  * @param {HTMLElement} icon Icon element to be setup.
617  * @param {VolumeInfo} volumeInfo VolumeInfo determines the icon type.
618  * @private
619  */
620 VolumeItem.prototype.setupIcon_ = function(icon, volumeInfo) {
621   icon.classList.add('volume-icon');
622   if (volumeInfo.volumeType === 'provided') {
623     var backgroundImage = '-webkit-image-set(' +
624         'url(chrome://extension-icon/' + volumeInfo.extensionId +
625             '/24/1) 1x, ' +
626         'url(chrome://extension-icon/' + volumeInfo.extensionId +
627             '/48/1) 2x);';
628     // The icon div is not yet added to DOM, therefore it is impossible to
629     // use style.backgroundImage.
630     icon.setAttribute(
631         'style', 'background-image: ' + backgroundImage);
632   }
633   icon.setAttribute('volume-type-icon', volumeInfo.volumeType);
634   icon.setAttribute('volume-subtype', volumeInfo.deviceType);
635 };
636
637 /**
638  * Set up eject button if needed.
639  * @param {HTMLElement} rowElement The parent element for eject button.
640  * @private
641  */
642 VolumeItem.prototype.setupEjectButton_ = function(rowElement) {
643   if (this.isRemovable_()) {
644     var ejectButton = cr.doc.createElement('div');
645     // Block other mouse handlers.
646     ejectButton.addEventListener(
647         'mouseup', function(event) { event.stopPropagation() });
648     ejectButton.addEventListener(
649         'mousedown', function(event) { event.stopPropagation() });
650     ejectButton.className = 'root-eject';
651     ejectButton.addEventListener('click', function(event) {
652       event.stopPropagation();
653       var unmountCommand = cr.doc.querySelector('command#unmount');
654       // Let's make sure 'canExecute' state of the command is properly set for
655       // the root before executing it.
656       unmountCommand.canExecuteChange(this);
657       unmountCommand.execute(this);
658     }.bind(this));
659     rowElement.appendChild(ejectButton);
660   }
661 };
662
663 ////////////////////////////////////////////////////////////////////////////////
664 // ShortcutItem
665
666 /**
667  * A TreeItem which represents a shortcut for Drive folder.
668  * Shortcut items are displayed as top-level children of DirectoryTree.
669  *
670  * @param {NavigationModelItem} modelItem NavigationModelItem of this volume.
671  * @param {DirectoryTree} tree Current tree, which contains this item.
672  * @extends {cr.ui.TreeItem}
673  * @constructor
674  */
675 function ShortcutItem(modelItem, tree) {
676   var item = new cr.ui.TreeItem();
677   item.__proto__ = ShortcutItem.prototype;
678   item.decorate(modelItem, tree);
679   return item;
680 }
681
682 ShortcutItem.prototype = {
683   __proto__: cr.ui.TreeItem.prototype,
684   get entry() {
685     return this.dirEntry_;
686   },
687   get modelItem() {
688     return this.modelItem_;
689   },
690   get labelElement() {
691     return this.firstElementChild.querySelector('.label');
692   }
693 };
694
695 /**
696  * Finds a parent directory of the {@code entry} in {@code this}, and
697  * invokes the DirectoryItem.selectByEntry() of the found directory.
698  *
699  * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
700  *     a fake.
701  * @return {boolean} True if the parent item is found.
702  */
703 ShortcutItem.prototype.searchAndSelectByEntry = function(entry) {
704   // Always false as shortcuts have no children.
705   return false;
706 };
707
708 /**
709  * Decorates this element.
710  * @param {NavigationModelItem} modelItem NavigationModelItem of this volume.
711  * @param {DirectoryTree} tree Current tree, which contains this item.
712  */
713 ShortcutItem.prototype.decorate = function(modelItem, tree) {
714   this.innerHTML = TREE_ITEM_INNTER_HTML;
715   this.parentTree_ = tree;
716   this.label = modelItem.entry.name;
717   this.dirEntry_ = modelItem.entry;
718   this.modelItem_ = modelItem;
719
720   var icon = this.querySelector('.icon');
721   icon.classList.add('volume-icon');
722   icon.setAttribute('volume-type-icon', VolumeManagerCommon.VolumeType.DRIVE);
723
724   if (tree.contextMenuForRootItems)
725     this.setContextMenu(tree.contextMenuForRootItems);
726 };
727
728 /**
729  * Invoked when the tree item is clicked.
730  *
731  * @param {Event} e Click event.
732  * @override
733  */
734 ShortcutItem.prototype.handleClick = function(e) {
735   cr.ui.TreeItem.prototype.handleClick.call(this, e);
736   this.parentTree_.directoryModel.clearSelection();
737 };
738
739 /**
740  * Select the item corresponding to the given entry.
741  * @param {DirectoryEntry} entry The directory entry to be selected.
742  */
743 ShortcutItem.prototype.selectByEntry = function(entry) {
744   if (util.isSameEntry(entry, this.entry))
745     this.selected = true;
746 };
747
748 /**
749  * Sets the context menu for shortcut items.
750  * @param {cr.ui.Menu} menu Menu to be set.
751  */
752 ShortcutItem.prototype.setContextMenu = function(menu) {
753   cr.ui.contextMenuHandler.setContextMenu(this, menu);
754 };
755
756 /**
757  * Change current entry to the entry corresponding to this shortcut.
758  */
759 ShortcutItem.prototype.activate = function() {
760   var directoryModel = this.parentTree_.directoryModel;
761   var onEntryResolved = function(entry) {
762     // Changes directory to the model item's root directory if needed.
763     if (!util.isSameEntry(directoryModel.getCurrentDirEntry(), entry)) {
764       metrics.recordUserAction('FolderShortcut.Navigate');
765       directoryModel.changeDirectoryEntry(entry);
766     }
767   }.bind(this);
768
769   // For shortcuts we already have an Entry, but it has to be resolved again
770   // in case, it points to a non-existing directory.
771   webkitResolveLocalFileSystemURL(
772       this.entry.toURL(),
773       onEntryResolved,
774       function() {
775         // Error, the entry can't be re-resolved. It may happen for shortcuts
776         // which targets got removed after resolving the Entry during
777         // initialization.
778         this.parentTree_.dataModel.onItemNotFoundError(this.modelItem);
779       }.bind(this));
780 };
781
782 ////////////////////////////////////////////////////////////////////////////////
783 // DirectoryTree
784
785 /**
786  * Tree of directories on the middle bar. This element is also the root of
787  * items, in other words, this is the parent of the top-level items.
788  *
789  * @constructor
790  * @extends {cr.ui.Tree}
791  */
792 function DirectoryTree() {}
793
794 /**
795  * Decorates an element.
796  * @param {HTMLElement} el Element to be DirectoryTree.
797  * @param {DirectoryModel} directoryModel Current DirectoryModel.
798  * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
799  * @param {MetadataCache} metadataCache Shared MetadataCache instance.
800  * @param {boolean} fakeEntriesVisible True if it should show the fakeEntries.
801  */
802 DirectoryTree.decorate = function(
803     el, directoryModel, volumeManager, metadataCache, fakeEntriesVisible) {
804   el.__proto__ = DirectoryTree.prototype;
805   (/** @type {DirectoryTree} */ el).decorate(
806       directoryModel, volumeManager, metadataCache, fakeEntriesVisible);
807 };
808
809 DirectoryTree.prototype = {
810   __proto__: cr.ui.Tree.prototype,
811
812   // DirectoryTree is always expanded.
813   get expanded() { return true; },
814   /**
815    * @param {boolean} value Not used.
816    */
817   set expanded(value) {},
818
819   /**
820    * The DirectoryEntry corresponding to this DirectoryItem. This may be
821    * a dummy DirectoryEntry.
822    * @type {DirectoryEntry|Object}
823    * @override
824    **/
825   get entry() {
826     return this.dirEntry_;
827   },
828
829   /**
830    * The DirectoryModel this tree corresponds to.
831    * @type {DirectoryModel}
832    */
833   get directoryModel() {
834     return this.directoryModel_;
835   },
836
837   /**
838    * The VolumeManager instance of the system.
839    * @type {VolumeManager}
840    */
841   get volumeManager() {
842     return this.volumeManager_;
843   },
844
845   /**
846    * The reference to shared MetadataCache instance.
847    * @type {MetadataCache}
848    */
849   get metadataCache() {
850     return this.metadataCache_;
851   },
852
853   set dataModel(dataModel) {
854     if (!this.onListContentChangedBound_)
855       this.onListContentChangedBound_ = this.onListContentChanged_.bind(this);
856
857     if (this.dataModel_) {
858       this.dataModel_.removeEventListener(
859           'change', this.onListContentChangedBound_);
860       this.dataModel_.removeEventListener(
861           'permuted', this.onListContentChangedBound_);
862     }
863     this.dataModel_ = dataModel;
864     dataModel.addEventListener('change', this.onListContentChangedBound_);
865     dataModel.addEventListener('permuted', this.onListContentChangedBound_);
866   },
867
868   get dataModel() {
869     return this.dataModel_;
870   }
871 };
872
873 cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
874 cr.defineProperty(DirectoryTree, 'contextMenuForRootItems', cr.PropertyKind.JS);
875
876 /**
877  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
878  *
879  * @param {boolean} recursive True if the all visible sub-directories are
880  *     updated recursively including left arrows. If false, the update walks
881  *     only immediate child directories without arrows.
882  */
883 DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
884   // First, current items which is not included in the dataModel should be
885   // removed.
886   for (var i = 0; i < this.items.length;) {
887     var found = false;
888     for (var j = 0; j < this.dataModel.length; j++) {
889       if (NavigationModelItem.isSame(this.items[i].modelItem,
890                                      this.dataModel.item(j))) {
891         found = true;
892         break;
893       }
894     }
895     if (!found) {
896       if (this.items[i].selected)
897         this.items[i].selected = false;
898       this.remove(this.items[i]);
899     } else {
900       i++;
901     }
902   }
903
904   // Next, insert items which is in dataModel but not in current items.
905   var modelIndex = 0;
906   var itemIndex = 0;
907   while (modelIndex < this.dataModel.length) {
908     if (itemIndex < this.items.length &&
909         NavigationModelItem.isSame(this.items[itemIndex].modelItem,
910                                    this.dataModel.item(modelIndex))) {
911       if (recursive && this.items[itemIndex] instanceof VolumeItem)
912         this.items[itemIndex].updateSubDirectories(true);
913     } else {
914       var modelItem = this.dataModel.item(modelIndex);
915       if (modelItem.isVolume)
916         this.addAt(new VolumeItem(modelItem, this), itemIndex);
917       else
918         this.addAt(new ShortcutItem(modelItem, this), itemIndex);
919     }
920     itemIndex++;
921     modelIndex++;
922   }
923
924   if (itemIndex !== 0)
925     this.hasChildren = true;
926 };
927
928 /**
929  * Finds a parent directory of the {@code entry} in {@code this}, and
930  * invokes the DirectoryItem.selectByEntry() of the found directory.
931  *
932  * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
933  *     a fake.
934  * @return {boolean} True if the parent item is found.
935  */
936 DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
937   // If the |entry| is same as one of volumes or shortcuts, select it.
938   for (var i = 0; i < this.items.length; i++) {
939     // Skips the Drive root volume. For Drive entries, one of children of Drive
940     // root or shortcuts should be selected.
941     var item = this.items[i];
942     if (item instanceof VolumeItem && item.isDrive())
943       continue;
944
945     if (util.isSameEntry(item.entry, entry)) {
946       this.dontHandleChangeEvent_ = true;
947       item.selectByEntry(entry);
948       this.dontHandleChangeEvent_ = false;
949       return true;
950     }
951   }
952   // Otherwise, search whole tree.
953   this.dontHandleChangeEvent_ = true;
954   var found = DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(
955       this, entry);
956   this.dontHandleChangeEvent_ = false;
957   return found;
958 };
959
960 /**
961  * Decorates an element.
962  * @param {DirectoryModel} directoryModel Current DirectoryModel.
963  * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
964  * @param {MetadataCache} metadataCache Shared MetadataCache instance.
965  * @param {boolean} fakeEntriesVisible True if it should show the fakeEntries.
966  */
967 DirectoryTree.prototype.decorate = function(
968     directoryModel, volumeManager, metadataCache, fakeEntriesVisible) {
969   cr.ui.Tree.prototype.decorate.call(this);
970
971   this.sequence_ = 0;
972   this.directoryModel_ = directoryModel;
973   this.volumeManager_ = volumeManager;
974   this.metadataCache_ = metadataCache;
975   this.models_ = [];
976
977   this.fileFilter_ = this.directoryModel_.getFileFilter();
978   this.fileFilter_.addEventListener('changed',
979                                     this.onFilterChanged_.bind(this));
980
981   this.directoryModel_.addEventListener('directory-changed',
982       this.onCurrentDirectoryChanged_.bind(this));
983
984   // Add a handler for directory change.
985   this.addEventListener('change', function() {
986     if (this.selectedItem && !this.dontHandleChangeEvent_)
987       this.selectedItem.activate();
988   }.bind(this));
989
990   this.privateOnDirectoryChangedBound_ =
991       this.onDirectoryContentChanged_.bind(this);
992   chrome.fileManagerPrivate.onDirectoryChanged.addListener(
993       this.privateOnDirectoryChangedBound_);
994
995   this.scrollBar_ = new MainPanelScrollBar();
996   this.scrollBar_.initialize(this.parentNode, this);
997
998   /**
999    * Flag to show fake entries in the tree.
1000    * @type {boolean}
1001    * @private
1002    */
1003   this.fakeEntriesVisible_ = fakeEntriesVisible;
1004 };
1005
1006 /**
1007  * Select the item corresponding to the given entry.
1008  * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
1009  *     be a fake.
1010  */
1011 DirectoryTree.prototype.selectByEntry = function(entry) {
1012   if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
1013     return;
1014
1015   if (this.searchAndSelectByEntry(entry))
1016     return;
1017
1018   this.updateSubDirectories(false /* recursive */);
1019   var currentSequence = ++this.sequence_;
1020   var volumeInfo = this.volumeManager_.getVolumeInfo(entry);
1021   if (!volumeInfo)
1022     return;
1023   volumeInfo.resolveDisplayRoot(function() {
1024     if (this.sequence_ !== currentSequence)
1025       return;
1026     if (!this.searchAndSelectByEntry(entry))
1027       this.selectedItem = null;
1028   }.bind(this));
1029 };
1030
1031 /**
1032  * Select the volume or the shortcut corresponding to the given index.
1033  * @param {number} index 0-based index of the target top-level item.
1034  * @return {boolean} True if one of the volume items is selected.
1035  */
1036 DirectoryTree.prototype.selectByIndex = function(index) {
1037   if (index < 0 || index >= this.items.length)
1038     return false;
1039
1040   this.items[index].selected = true;
1041   return true;
1042 };
1043
1044 /**
1045  * Retrieves the latest subdirectories and update them on the tree.
1046  *
1047  * @param {boolean} recursive True if the update is recursively.
1048  * @param {function()=} opt_callback Called when subdirectories are fully
1049  *     updated.
1050  */
1051 DirectoryTree.prototype.updateSubDirectories = function(
1052     recursive, opt_callback) {
1053   this.redraw(recursive);
1054   if (opt_callback)
1055     opt_callback();
1056 };
1057
1058 /**
1059  * Redraw the list.
1060  * @param {boolean} recursive True if the update is recursively. False if the
1061  *     only root items are updated.
1062  */
1063 DirectoryTree.prototype.redraw = function(recursive) {
1064   this.updateSubElementsFromList(recursive);
1065 };
1066
1067 /**
1068  * Invoked when the filter is changed.
1069  * @private
1070  */
1071 DirectoryTree.prototype.onFilterChanged_ = function() {
1072   // Returns immediately, if the tree is hidden.
1073   if (this.hidden)
1074     return;
1075
1076   this.redraw(true /* recursive */);
1077 };
1078
1079 /**
1080  * Invoked when a directory is changed.
1081  * @param {!UIEvent} event Event.
1082  * @private
1083  */
1084 DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
1085   if (event.eventType !== 'changed')
1086     return;
1087
1088   for (var i = 0; i < this.items.length; i++) {
1089     if (this.items[i] instanceof VolumeItem)
1090       this.items[i].updateItemByEntry(event.entry);
1091   }
1092 };
1093
1094 /**
1095  * Invoked when the current directory is changed.
1096  * @param {!UIEvent} event Event.
1097  * @private
1098  */
1099 DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
1100   this.selectByEntry(event.newDirEntry);
1101 };
1102
1103 /**
1104  * Invoked when the volume list or shortcut list is changed.
1105  * @private
1106  */
1107 DirectoryTree.prototype.onListContentChanged_ = function() {
1108   this.updateSubDirectories(false, function() {
1109     // If no item is selected now, try to select the item corresponding to
1110     // current directory because the current directory might have been populated
1111     // in this tree in previous updateSubDirectories().
1112     if (!this.selectedItem) {
1113       var currentDir = this.directoryModel_.getCurrentDirEntry();
1114       if (currentDir)
1115         this.selectByEntry(currentDir);
1116     }
1117   }.bind(this));
1118 };
1119
1120 /**
1121  * Updates the UI after the layout has changed.
1122  */
1123 DirectoryTree.prototype.relayout = function() {
1124   cr.dispatchSimpleEvent(this, 'relayout');
1125 };