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