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