Upstream version 5.34.92.0
[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 // 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
33     var locationInfo = tree.volumeManager_.getLocationInfo(currentEntry);
34     var label;
35     if (locationInfo && locationInfo.isRootEntry)
36       label = PathUtil.getRootTypeLabel(locationInfo.rootType);
37     else
38       label = currentEntry.name;
39
40     if (index >= this.items.length) {
41       var item = new DirectoryItem(label, currentEntry, this, tree);
42       this.add(item);
43       index++;
44     } else if (util.isSameEntry(currentEntry, currentElement.entry)) {
45       if (recursive && this.expanded)
46         currentElement.updateSubDirectories(true /* recursive */);
47
48       index++;
49     } else if (currentEntry.toURL() < currentElement.entry.toURL()) {
50       var item = new DirectoryItem(label, currentEntry, this, tree);
51       this.addAt(item, index);
52       index++;
53     } else if (currentEntry.toURL() > currentElement.entry.toURL()) {
54       this.remove(currentElement);
55     }
56   }
57
58   var removedChild;
59   while (removedChild = this.items[index]) {
60     this.remove(removedChild);
61   }
62
63   if (index === 0) {
64     this.hasChildren = false;
65     this.expanded = false;
66   } else {
67     this.hasChildren = true;
68   }
69 };
70
71 /**
72  * Finds a parent directory of the {@code entry} in {@code this}, and
73  * invokes the DirectoryItem.selectByEntry() of the found directory.
74  *
75  * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
76  *     a fake.
77  * @return {boolean} True if the parent item is found.
78  */
79 DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
80   for (var i = 0; i < this.items.length; i++) {
81     var item = this.items[i];
82     if (util.isDescendantEntry(item.entry, entry) ||
83         util.isSameEntry(item.entry, entry)) {
84       item.selectByEntry(entry);
85       return true;
86     }
87   }
88   return false;
89 };
90
91 Object.freeze(DirectoryItemTreeBaseMethods);
92
93 ////////////////////////////////////////////////////////////////////////////////
94 // DirectoryItem
95
96 /**
97  * A directory in the tree. Each element represents one directory.
98  *
99  * @param {string} label Label for this item.
100  * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
101  * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
102  * @param {DirectoryTree} tree Current tree, which contains this item.
103  * @extends {cr.ui.TreeItem}
104  * @constructor
105  */
106 function DirectoryItem(label, dirEntry, parentDirItem, tree) {
107   var item = new cr.ui.TreeItem();
108   DirectoryItem.decorate(item, label, dirEntry, parentDirItem, tree);
109   return item;
110 }
111
112 /**
113  * @param {HTMLElement} el Element to be DirectoryItem.
114  * @param {string} label Label for this item.
115  * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
116  * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
117  * @param {DirectoryTree} tree Current tree, which contains this item.
118  */
119 DirectoryItem.decorate =
120     function(el, label, dirEntry, parentDirItem, tree) {
121   el.__proto__ = DirectoryItem.prototype;
122   (/** @type {DirectoryItem} */ el).decorate(
123       label, dirEntry, parentDirItem, tree);
124 };
125
126 DirectoryItem.prototype = {
127   __proto__: cr.ui.TreeItem.prototype,
128
129   /**
130    * The DirectoryEntry corresponding to this DirectoryItem. This may be
131    * a dummy DirectoryEntry.
132    * @type {DirectoryEntry|Object}
133    */
134   get entry() {
135     return this.dirEntry_;
136   },
137
138   /**
139    * The element containing the label text and the icon.
140    * @type {!HTMLElement}
141    * @override
142    */
143   get labelElement() {
144     return this.firstElementChild.querySelector('.label');
145   }
146 };
147
148 /**
149  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
150  *
151  * @param {boolean} recursive True if the all visible sub-directories are
152  *     updated recursively including left arrows. If false, the update walks
153  *     only immediate child directories without arrows.
154  */
155 DirectoryItem.prototype.updateSubElementsFromList = function(recursive) {
156   DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
157 };
158
159 /**
160  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
161  *
162  * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
163  *     a fake.
164  * @return {boolean} True if the parent item is found.
165  */
166 DirectoryItem.prototype.searchAndSelectByEntry = function(entry) {
167   return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
168 };
169
170 /**
171  * @param {string} label Localized label for this item.
172  * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
173  * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
174  * @param {DirectoryTree} tree Current tree, which contains this item.
175  */
176 DirectoryItem.prototype.decorate = function(
177     label, dirEntry, parentDirItem, tree) {
178   this.innerHTML =
179       '<div class="tree-row">' +
180       ' <span class="expand-icon"></span>' +
181       ' <span class="icon"></span>' +
182       ' <span class="label"></span>' +
183       '</div>' +
184       '<div class="tree-children"></div>';
185
186   this.parentTree_ = tree;
187   this.directoryModel_ = tree.directoryModel;
188   this.parent_ = parentDirItem;
189   this.label = label;
190   this.dirEntry_ = dirEntry;
191   this.fileFilter_ = this.directoryModel_.getFileFilter();
192
193   // Sets hasChildren=false tentatively. This will be overridden after
194   // scanning sub-directories in updateSubElementsFromList().
195   this.hasChildren = false;
196
197   this.addEventListener('expand', this.onExpand_.bind(this), false);
198   var icon = this.querySelector('.icon');
199   icon.classList.add('volume-icon');
200   var location = tree.volumeManager.getLocationInfo(dirEntry);
201   if (location && location.rootType && location.isRootEntry)
202     icon.setAttribute('volume-type-icon', location.rootType);
203   else
204     icon.setAttribute('file-type-icon', 'folder');
205
206   if (this.parentTree_.contextMenuForSubitems)
207     this.setContextMenu(this.parentTree_.contextMenuForSubitems);
208   // Adds handler for future change.
209   this.parentTree_.addEventListener(
210       'contextMenuForSubitemsChange',
211       function(e) { this.setContextMenu(e.newValue); }.bind(this));
212
213   if (parentDirItem.expanded)
214     this.updateSubDirectories(false /* recursive */);
215 };
216
217 /**
218  * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
219  * a complex layout. This call is not necessary, so we are ignoring it.
220  *
221  * @param {boolean} unused Unused.
222  * @override
223  */
224 DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
225 };
226
227 /**
228  * Removes the child node, but without selecting the parent item, to avoid
229  * unintended changing of directories. Removing is done externally, and other
230  * code will navigate to another directory.
231  *
232  * @param {!cr.ui.TreeItem} child The tree item child to remove.
233  * @override
234  */
235 DirectoryItem.prototype.remove = function(child) {
236   this.lastElementChild.removeChild(child);
237   if (this.items.length == 0)
238     this.hasChildren = false;
239 };
240
241 /**
242  * Invoked when the item is being expanded.
243  * @param {!UIEvent} e Event.
244  * @private
245  **/
246 DirectoryItem.prototype.onExpand_ = function(e) {
247   this.updateSubDirectories(
248       true /* recursive */,
249       function() {},
250       function() {
251         this.expanded = false;
252       }.bind(this));
253
254   e.stopPropagation();
255 };
256
257 /**
258  * Retrieves the latest subdirectories and update them on the tree.
259  * @param {boolean} recursive True if the update is recursively.
260  * @param {function()=} opt_successCallback Callback called on success.
261  * @param {function()=} opt_errorCallback Callback called on error.
262  */
263 DirectoryItem.prototype.updateSubDirectories = function(
264     recursive, opt_successCallback, opt_errorCallback) {
265   if (util.isFakeEntry(this.entry)) {
266     if (opt_errorCallback)
267       opt_errorCallback();
268     return;
269   }
270
271   var sortEntries = function(fileFilter, entries) {
272     entries.sort(function(a, b) {
273       return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1;
274     });
275     return entries.filter(fileFilter.filter.bind(fileFilter));
276   };
277
278   var onSuccess = function(entries) {
279     this.entries_ = entries;
280     this.redrawSubDirectoryList_(recursive);
281     opt_successCallback && opt_successCallback();
282   }.bind(this);
283
284   var reader = this.entry.createReader();
285   var entries = [];
286   var readEntry = function() {
287     reader.readEntries(function(results) {
288       if (!results.length) {
289         onSuccess(sortEntries(this.fileFilter_, entries));
290         return;
291       }
292
293       for (var i = 0; i < results.length; i++) {
294         var entry = results[i];
295         if (entry.isDirectory)
296           entries.push(entry);
297       }
298       readEntry();
299     }.bind(this));
300   }.bind(this);
301   readEntry();
302 };
303
304 /**
305  * Searches for the changed directory in the current subtree, and if it is found
306  * then updates it.
307  *
308  * @param {DirectoryEntry} changedDirectoryEntry The entry ot the changed
309  *     directory.
310  */
311 DirectoryItem.prototype.updateItemByEntry = function(changedDirectoryEntry) {
312   if (util.isSameEntry(changedDirectoryEntry, this.entry)) {
313     this.updateSubDirectories(false /* recursive */);
314     return;
315   }
316
317   // Traverse the entire subtree to find the changed element.
318   for (var i = 0; i < this.items.length; i++) {
319     var item = this.items[i];
320     if (util.isDescendantEntry(item.entry, changedDirectoryEntry)) {
321       item.updateItemByEntry(changedDirectoryEntry);
322       break;
323     }
324   }
325 };
326
327 /**
328  * Redraw subitems with the latest information. The items are sorted in
329  * alphabetical order, case insensitive.
330  * @param {boolean} recursive True if the update is recursively.
331  * @private
332  */
333 DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
334   this.updateSubElementsFromList(recursive);
335 };
336
337 /**
338  * Select the item corresponding to the given {@code entry}.
339  * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
340  */
341 DirectoryItem.prototype.selectByEntry = function(entry) {
342   if (util.isSameEntry(entry, this.entry)) {
343     this.selected = true;
344     return;
345   }
346
347   if (this.searchAndSelectByEntry(entry))
348     return;
349
350   // If the entry doesn't exist, updates sub directories and tries again.
351   this.updateSubDirectories(
352       false /* recursive */,
353       this.searchAndSelectByEntry.bind(this, entry));
354 };
355
356 /**
357  * Executes the assigned action as a drop target.
358  */
359 DirectoryItem.prototype.doDropTargetAction = function() {
360   this.expanded = true;
361 };
362
363 /**
364  * Executes the assigned action. DirectoryItem performs changeDirectory.
365  */
366 DirectoryItem.prototype.doAction = function() {
367   if (!this.directoryModel_.getCurrentDirEntry() ||
368       !util.isSameEntry(this.entry,
369                         this.directoryModel_.getCurrentDirEntry())) {
370     this.directoryModel_.changeDirectoryEntry(this.entry);
371   }
372 };
373
374 /**
375  * Sets the context menu for directory tree.
376  * @param {cr.ui.Menu} menu Menu to be set.
377  */
378 DirectoryItem.prototype.setContextMenu = function(menu) {
379   var tree = this.parentTree_ || this;  // If no parent, 'this' itself is tree.
380   var locationInfo = tree.volumeManager_.getLocationInfo(this.entry);
381   if (locationInfo && locationInfo.isEligibleForFolderShortcut)
382     cr.ui.contextMenuHandler.setContextMenu(this, menu);
383 };
384
385 ////////////////////////////////////////////////////////////////////////////////
386 // DirectoryTree
387
388 /**
389  * Tree of directories on the middle bar. This element is also the root of
390  * items, in other words, this is the parent of the top-level items.
391  *
392  * @constructor
393  * @extends {cr.ui.Tree}
394  */
395 function DirectoryTree() {}
396
397 /**
398  * Decorates an element.
399  * @param {HTMLElement} el Element to be DirectoryTree.
400  * @param {DirectoryModel} directoryModel Current DirectoryModel.
401  * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
402  */
403 DirectoryTree.decorate = function(el, directoryModel, volumeManager) {
404   el.__proto__ = DirectoryTree.prototype;
405   (/** @type {DirectoryTree} */ el).decorate(directoryModel, volumeManager);
406 };
407
408 DirectoryTree.prototype = {
409   __proto__: cr.ui.Tree.prototype,
410
411   // DirectoryTree is always expanded.
412   get expanded() { return true; },
413   /**
414    * @param {boolean} value Not used.
415    */
416   set expanded(value) {},
417
418   /**
419    * The DirectoryEntry corresponding to this DirectoryItem. This may be
420    * a dummy DirectoryEntry.
421    * @type {DirectoryEntry|Object}
422    * @override
423    **/
424   get entry() {
425     return this.dirEntry_;
426   },
427
428   /**
429    * The DirectoryModel this tree corresponds to.
430    * @type {DirectoryModel}
431    */
432   get directoryModel() {
433     return this.directoryModel_;
434   },
435
436   /**
437    * The VolumeManager instance of the system.
438    * @type {VolumeManager}
439    */
440   get volumeManager() {
441     return this.volumeManager_;
442   },
443 };
444
445 cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
446
447 /**
448  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
449  *
450  * @param {boolean} recursive True if the all visible sub-directories are
451  *     updated recursively including left arrows. If false, the update walks
452  *     only immediate child directories without arrows.
453  */
454 DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
455   DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
456 };
457
458 /**
459  * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
460  *
461  * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
462  *     a fake.
463  * @return {boolean} True if the parent item is found.
464  */
465 DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
466   return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
467 };
468
469 /**
470  * Decorates an element.
471  * @param {DirectoryModel} directoryModel Current DirectoryModel.
472  * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
473  */
474 DirectoryTree.prototype.decorate = function(directoryModel, volumeManager) {
475   cr.ui.Tree.prototype.decorate.call(this);
476
477   this.sequence_ = 0;
478   this.directoryModel_ = directoryModel;
479   this.volumeManager_ = volumeManager;
480   this.entries_ = [];
481   this.currentVolumeInfo_ = null;
482
483   this.fileFilter_ = this.directoryModel_.getFileFilter();
484   this.fileFilter_.addEventListener('changed',
485                                     this.onFilterChanged_.bind(this));
486
487   this.directoryModel_.addEventListener('directory-changed',
488       this.onCurrentDirectoryChanged_.bind(this));
489
490   // Add a handler for directory change.
491   this.addEventListener('change', function() {
492     if (this.selectedItem &&
493         (!this.currentEntry_ ||
494          !util.isSameEntry(this.currentEntry_, this.selectedItem.entry))) {
495       this.currentEntry_ = this.selectedItem.entry;
496       this.selectedItem.doAction();
497       return;
498     }
499   }.bind(this));
500
501   this.privateOnDirectoryChangedBound_ =
502       this.onDirectoryContentChanged_.bind(this);
503   chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
504       this.privateOnDirectoryChangedBound_);
505
506   this.scrollBar_ = MainPanelScrollBar();
507   this.scrollBar_.initialize(this.parentNode, this);
508 };
509
510 /**
511  * Select the item corresponding to the given entry.
512  * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
513  *     be a fake.
514  */
515 DirectoryTree.prototype.selectByEntry = function(entry) {
516   // If the target directory is not in the tree, do nothing.
517   var locationInfo = this.volumeManager_.getLocationInfo(entry);
518   if (!locationInfo || !locationInfo.isDriveBased)
519     return;
520
521   var volumeInfo = this.volumeManager_.getVolumeInfo(entry);
522   if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
523     return;
524
525   if (this.searchAndSelectByEntry(entry))
526     return;
527
528   this.updateSubDirectories(false /* recursive */);
529   var currentSequence = ++this.sequence_;
530   volumeInfo.resolveDisplayRoot(function() {
531     if (this.sequence_ !== currentSequence)
532       return;
533     if (!this.searchAndSelectByEntry(entry))
534       this.selectedItem = null;
535   }.bind(this));
536 };
537
538 /**
539  * Retrieves the latest subdirectories and update them on the tree.
540  *
541  * @param {boolean} recursive True if the update is recursively.
542  * @param {function()=} opt_callback Called when subdirectories are fully
543  *     updated.
544  */
545 DirectoryTree.prototype.updateSubDirectories = function(
546     recursive, opt_callback) {
547   var callback = opt_callback || function() {};
548   this.entries_ = [];
549
550   var compareEntries = function(a, b) {
551     return a.toURL() < b.toURL();
552   };
553
554   // Add fakes (if any).
555   for (var key in this.currentVolumeInfo_.fakeEntries) {
556     this.entries_.push(this.currentVolumeInfo_.fakeEntries[key]);
557   }
558
559   // If the display root is not available yet, then redraw anyway with what
560   // we have. However, concurrently try to resolve the display root and then
561   // redraw.
562   if (!this.currentVolumeInfo_.displayRoot) {
563     this.entries_.sort(compareEntries);
564     this.redraw(recursive);
565   }
566
567   this.currentVolumeInfo_.resolveDisplayRoot(function(displayRoot) {
568     this.entries_.push(this.currentVolumeInfo_.displayRoot);
569     this.entries_.sort(compareEntries);
570     this.redraw(recursive);  // Redraw.
571     callback();
572   }.bind(this), callback /* Ignore errors. */);
573 };
574
575 /**
576  * Redraw the list.
577  * @param {boolean} recursive True if the update is recursively. False if the
578  *     only root items are updated.
579  */
580 DirectoryTree.prototype.redraw = function(recursive) {
581   this.updateSubElementsFromList(recursive);
582 };
583
584 /**
585  * Invoked when the filter is changed.
586  * @private
587  */
588 DirectoryTree.prototype.onFilterChanged_ = function() {
589   // Returns immediately, if the tree is hidden.
590   if (this.hidden)
591     return;
592
593   this.redraw(true /* recursive */);
594 };
595
596 /**
597  * Invoked when a directory is changed.
598  * @param {!UIEvent} event Event.
599  * @private
600  */
601 DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
602   if (event.eventType !== 'changed')
603     return;
604
605   var locationInfo = this.volumeManager_.getLocationInfo(event.entry);
606   if (!locationInfo || !locationInfo.isDriveBased)
607     return;
608
609   var myDriveItem = this.items[0];
610   if (myDriveItem)
611     myDriveItem.updateItemByEntry(event.entry);
612 };
613
614 /**
615  * Invoked when the current directory is changed.
616  * @param {!UIEvent} event Event.
617  * @private
618  */
619 DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
620   this.currentVolumeInfo_ =
621       this.volumeManager_.getVolumeInfo(event.newDirEntry);
622   this.selectByEntry(event.newDirEntry);
623 };
624
625 /**
626  * Sets the margin height for the transparent preview panel at the bottom.
627  * @param {number} margin Margin to be set in px.
628  */
629 DirectoryTree.prototype.setBottomMarginForPanel = function(margin) {
630   this.style.paddingBottom = margin + 'px';
631   this.scrollBar_.setBottomMarginForPanel(margin);
632 };
633
634 /**
635  * Updates the UI after the layout has changed.
636  */
637 DirectoryTree.prototype.relayout = function() {
638   cr.dispatchSimpleEvent(this, 'relayout');
639 };