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