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