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