1 // Copyright (c) 2011 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 // WK Bug 55728 is fixed on the chrome 12 branch but not on the trunk. 6 // TODO(rginda): Enable this everywhere once we have a trunk-worthy fix. 7 const ENABLE_EXIF_READER = navigator.userAgent.match(/chrome\/12\.0/i); 8 9 // Thumbnail view is painful without the exif reader. 10 const ENABLE_THUMBNAIL_VIEW = ENABLE_EXIF_READER; 11 12 var g_slideshow_data = null; 13 14 /** 15 * FileManager constructor. 16 * 17 * FileManager objects encapsulate the functionality of the file selector 18 * dialogs, as well as the full screen file manager application (though the 19 * latter is not yet implemented). 20 * 21 * @param {HTMLElement} dialogDom The DOM node containing the prototypical 22 * dialog UI. 23 * @param {DOMFileSystem} filesystem The HTML5 filesystem object representing 24 * the root filesystem for the new FileManager. 25 * @param {Object} params A map of parameter names to values controlling the 26 * appearance of the FileManager. Names are: 27 * - type: A value from FileManager.DialogType defining what kind of 28 * dialog to present. Defaults to FULL_PAGE. 29 * - title: The title for the dialog. Defaults to a localized string based 30 * on the dialog type. 31 * - defaultPath: The default path for the dialog. The default path should 32 * end with a trailing slash if it represents a directory. 33 */ 34 function FileManager(dialogDom, rootEntries, params) { 35 console.log('Init FileManager: ' + dialogDom); 36 37 this.dialogDom_ = dialogDom; 38 this.rootEntries_ = rootEntries; 39 this.filesystem_ = rootEntries[0].filesystem; 40 this.params_ = params || {}; 41 42 this.listType_ = null; 43 44 this.exifCache_ = {}; 45 46 // True if we should filter out files that start with a dot. 47 this.filterFiles_ = true; 48 49 this.commands_ = {}; 50 51 this.document_ = dialogDom.ownerDocument; 52 this.dialogType_ = 53 this.params_.type || FileManager.DialogType.FULL_PAGE; 54 55 this.defaultPath_ = this.params_.defaultPath || '/'; 56 57 // This is set to just the directory portion of defaultPath in initDialogType. 58 this.defaultFolder_ = '/'; 59 60 this.showCheckboxes_ = 61 (this.dialogType_ == FileManager.DialogType.FULL_PAGE || 62 this.dialogType_ == FileManager.DialogType.SELECT_OPEN_MULTI_FILE); 63 64 // DirectoryEntry representing the current directory of the dialog. 65 this.currentDirEntry_ = null; 66 67 window.addEventListener('popstate', this.onPopState_.bind(this)); 68 this.addEventListener('directory-changed', 69 this.onDirectoryChanged_.bind(this)); 70 this.addEventListener('selection-summarized', 71 this.onSelectionSummarized_.bind(this)); 72 73 this.initCommands_(); 74 this.initDom_(); 75 this.initDialogType_(); 76 77 this.summarizeSelection_(); 78 this.updatePreview_(); 79 this.changeDirectory(this.defaultFolder_); 80 81 chrome.fileBrowserPrivate.onDiskChanged.addListener( 82 this.onDiskChanged_.bind(this)); 83 84 this.table_.list_.focus(); 85 86 if (ENABLE_EXIF_READER) { 87 this.exifReader = new Worker('js/exif_reader.js'); 88 this.exifReader.onmessage = this.onExifReaderMessage_.bind(this); 89 this.exifReader.postMessage({verb: 'init'}); 90 } 91 } 92 93 FileManager.prototype = { 94 __proto__: cr.EventTarget.prototype 95 }; 96 97 // Anonymous "namespace". 98 (function() { 99 100 // Private variables and helper functions. 101 102 /** 103 * Unicode codepoint for 'BLACK RIGHT-POINTING SMALL TRIANGLE'. 104 */ 105 const RIGHT_TRIANGLE = '\u25b8'; 106 107 /** 108 * The DirectoryEntry.fullPath value of the directory containing external 109 * storage volumes. 110 */ 111 const MEDIA_DIRECTORY = '/media'; 112 113 /** 114 * Translated strings. 115 */ 116 var localStrings; 117 118 /** 119 * Map of icon types to regular expressions. 120 * 121 * The first regexp to match the file name determines the icon type 122 * assigned to dom elements for a file. Order of evaluation is not 123 * defined, so don't depend on it. 124 */ 125 const iconTypes = { 126 'audio': /\.(mp3|m4a|oga|ogg|wav)$/i, 127 'html': /\.(html?)$/i, 128 'image': /\.(bmp|gif|jpe?g|ico|png|webp)$/i, 129 'pdf' : /\.(pdf)$/i, 130 'text': /\.(pod|rst|txt|log)$/i, 131 'video': /\.(mov|mp4|m4v|mpe?g4?|ogm|ogv|ogx|webm)$/i 132 }; 133 134 const previewArt = { 135 'audio': 'images/filetype_large_audio.png', 136 'folder': 'images/filetype_large_folder.png', 137 'unknown': 'images/filetype_large_generic.png', 138 'video': 'images/filetype_large_video.png' 139 }; 140 141 /** 142 * Return a translated string. 143 * 144 * Wrapper function to make dealing with translated strings more concise. 145 * Equivilant to localStrings.getString(id). 146 * 147 * @param {string} id The id of the string to return. 148 * @return {string} The translated string. 149 */ 150 function str(id) { 151 return localStrings.getString(id); 152 } 153 154 /** 155 * Return a translated string with arguments replaced. 156 * 157 * Wrapper function to make dealing with translated strings more concise. 158 * Equivilant to localStrings.getStringF(id, ...). 159 * 160 * @param {string} id The id of the string to return. 161 * @param {...string} The values to replace into the string. 162 * @return {string} The translated string with replaced values. 163 */ 164 function strf(id, var_args) { 165 return localStrings.getStringF.apply(localStrings, arguments); 166 } 167 168 /** 169 * Checks if |parent_path| is parent file path of |child_path|. 170 * 171 * @param {string} parent_path The parent path. 172 * @param {string} child_path The child path. 173 */ 174 function isParentPath(parent_path, child_path) { 175 if (!parent_path || parent_path.length == 0 || 176 !child_path || child_path.length == 0) 177 return false; 178 179 if (parent_path[parent_path.length -1] != '/') 180 parent_path += '/'; 181 182 if (child_path[child_path.length -1] != '/') 183 child_path += '/'; 184 185 return child_path.indexOf(parent_path) == 0; 186 } 187 188 /** 189 * Returns parent folder path of file path. 190 * 191 * @param {string} path The file path. 192 */ 193 function getParentPath(path) { 194 var parent = path.replace(/[\/]?[^\/]+[\/]?$/,''); 195 if (parent.length == 0) 196 parent = '/'; 197 return parent; 198 } 199 200 /** 201 * Get the icon type for a given Entry. 202 * 203 * @param {Entry} entry An Entry subclass (FileEntry or DirectoryEntry). 204 * @return {string} One of the keys from FileManager.iconTypes, or 205 * 'unknown'. 206 */ 207 function getIconType(entry) { 208 if (entry.cachedIconType_) 209 return entry.cachedIconType_; 210 211 var rv = 'unknown'; 212 213 if (entry.isDirectory) { 214 rv = 'folder'; 215 } else { 216 for (var name in iconTypes) { 217 var value = iconTypes[name]; 218 219 if (value instanceof RegExp) { 220 if (value.test(entry.name)) { 221 rv = name; 222 break; 223 } 224 } else if (typeof value == 'function') { 225 try { 226 if (value(entry)) { 227 rv = name; 228 break; 229 } 230 } catch (ex) { 231 console.error('Caught exception while evaluating iconType: ' + 232 name, ex); 233 } 234 } else { 235 console.log('Unexpected value in iconTypes[' + name + ']: ' + value); 236 } 237 } 238 } 239 240 entry.cachedIconType_ = rv; 241 return rv; 242 } 243 244 /** 245 * Call an asynchronous method on dirEntry, batching multiple callers. 246 * 247 * This batches multiple callers into a single invocation, calling all 248 * interested parties back when the async call completes. 249 * 250 * The Entry method to be invoked should take two callbacks as parameters 251 * (one for success and one for failure), and it should invoke those 252 * callbacks with a single parameter representing the result of the call. 253 * Example methods are Entry.getMetadata() and FileEntry.file(). 254 * 255 * Warning: Because this method caches the first result, subsequent changes 256 * to the entry will not be visible to callers. 257 * 258 * Error results are never cached. 259 * 260 * @param {DirectoryEntry} dirEntry The DirectoryEntry to apply the method 261 * to. 262 * @param {string} methodName The name of the method to dispatch. 263 * @param {function(*)} successCallback The function to invoke if the method 264 * succeeds. The result of the method will be the one parameter to this 265 * callback. 266 * @param {function(*)} opt_errorCallback The function to invoke if the 267 * method fails. The result of the method will be the one parameter to 268 * this callback. If not provided, the default errorCallback will throw 269 * an exception. 270 */ 271 function batchAsyncCall(entry, methodName, successCallback, 272 opt_errorCallback) { 273 var resultCache = methodName + '_resultCache_'; 274 275 if (entry[resultCache]) { 276 // The result cache for this method already exists. Just invoke the 277 // successCallback with the result of the previuos call. 278 // Callback via a setTimeout so the sync/async semantics don't change 279 // based on whether or not the value is cached. 280 setTimeout(function() { successCallback(entry[resultCache]) }, 0); 281 return; 282 } 283 284 if (!opt_errorCallback) { 285 opt_errorCallback = util.ferr('Error calling ' + methodName + ' for: ' + 286 entry.fullPath); 287 } 288 289 var observerList = methodName + '_observers_'; 290 291 if (entry[observerList]) { 292 // The observer list already exists, indicating we have a pending call 293 // out to this method. Add this caller to the list of observers and 294 // bail out. 295 entry[observerList].push([successCallback, opt_errorCallback]); 296 return; 297 } 298 299 entry[observerList] = [[successCallback, opt_errorCallback]]; 300 301 function onComplete(success, result) { 302 if (success) 303 entry[resultCache] = result; 304 305 for (var i = 0; i < entry[observerList].length; i++) { 306 entry[observerList][i][success ? 0 : 1](result); 307 } 308 309 delete entry[observerList]; 310 }; 311 312 entry[methodName](function(rv) { onComplete(true, rv) }, 313 function(rv) { onComplete(false, rv) }); 314 } 315 316 /** 317 * Get the size of a file, caching the result. 318 * 319 * When this method completes, the fileEntry object will get a 320 * 'cachedSize_' property (if it doesn't already have one) containing the 321 * size of the file in bytes. 322 * 323 * @param {Entry} entry An HTML5 Entry object. 324 * @param {function(Entry)} successCallback The function to invoke once the 325 * file size is known. 326 */ 327 function cacheEntrySize(entry, successCallback) { 328 if (entry.isDirectory) { 329 // No size for a directory, -1 ensures it's sorted before 0 length files. 330 entry.cachedSize_ = -1; 331 } 332 333 if ('cachedSize_' in entry) { 334 if (successCallback) { 335 // Callback via a setTimeout so the sync/async semantics don't change 336 // based on whether or not the value is cached. 337 setTimeout(function() { successCallback(entry) }, 0); 338 } 339 return; 340 } 341 342 batchAsyncCall(entry, 'file', function(file) { 343 entry.cachedSize_ = file.size; 344 if (successCallback) 345 successCallback(entry); 346 }); 347 } 348 349 /** 350 * Get the mtime of a file, caching the result. 351 * 352 * When this method completes, the fileEntry object will get a 353 * 'cachedMtime_' property (if it doesn't already have one) containing the 354 * last modified time of the file as a Date object. 355 * 356 * @param {Entry} entry An HTML5 Entry object. 357 * @param {function(Entry)} successCallback The function to invoke once the 358 * mtime is known. 359 */ 360 function cacheEntryDate(entry, successCallback) { 361 if ('cachedMtime_' in entry) { 362 if (successCallback) { 363 // Callback via a setTimeout so the sync/async semantics don't change 364 // based on whether or not the value is cached. 365 setTimeout(function() { successCallback(entry) }, 0); 366 } 367 return; 368 } 369 370 if (entry.isFile) { 371 batchAsyncCall(entry, 'file', function(file) { 372 entry.cachedMtime_ = file.lastModifiedDate; 373 if (successCallback) 374 successCallback(entry); 375 }); 376 } else { 377 batchAsyncCall(entry, 'getMetadata', function(metadata) { 378 entry.cachedMtime_ = metadata.modificationTime; 379 if (successCallback) 380 successCallback(entry); 381 }); 382 } 383 } 384 385 /** 386 * Get the icon type of a file, caching the result. 387 * 388 * When this method completes, the fileEntry object will get a 389 * 'cachedIconType_' property (if it doesn't already have one) containing the 390 * icon type of the file as a string. 391 * 392 * The successCallback is always invoked synchronously, since this does not 393 * actually require an async call. You should not depend on this, as it may 394 * change if we were to start reading magic numbers (for example). 395 * 396 * @param {Entry} entry An HTML5 Entry object. 397 * @param {function(Entry)} successCallback The function to invoke once the 398 * icon type is known. 399 */ 400 function cacheEntryIconType(entry, successCallback) { 401 getIconType(entry); 402 if (successCallback) 403 setTimeout(function() { successCallback(entry) }, 0); 404 } 405 406 // Public statics. 407 408 /** 409 * List of dialog types. 410 * 411 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except 412 * FULL_PAGE which is specific to this code. 413 * 414 * @enum {string} 415 */ 416 FileManager.DialogType = { 417 SELECT_FOLDER: 'folder', 418 SELECT_SAVEAS_FILE: 'saveas-file', 419 SELECT_OPEN_FILE: 'open-file', 420 SELECT_OPEN_MULTI_FILE: 'open-multi-file', 421 FULL_PAGE: 'full-page' 422 }; 423 424 FileManager.ListType = { 425 DETAIL: 'detail', 426 THUMBNAIL: 'thumb' 427 }; 428 429 /** 430 * Load translated strings. 431 */ 432 FileManager.initStrings = function(callback) { 433 chrome.fileBrowserPrivate.getStrings(function(strings) { 434 localStrings = new LocalStrings(strings); 435 cr.initLocale(strings); 436 437 if (callback) 438 callback(); 439 }); 440 }; 441 442 // Instance methods. 443 444 /** 445 * One-time initialization of commands. 446 */ 447 FileManager.prototype.initCommands_ = function() { 448 var commands = this.dialogDom_.querySelectorAll('command'); 449 for (var i = 0; i < commands.length; i++) { 450 var command = commands[i]; 451 cr.ui.Command.decorate(command); 452 this.commands_[command.id] = command; 453 } 454 455 this.fileContextMenu_ = this.dialogDom_.querySelector('.file-context-menu'); 456 cr.ui.Menu.decorate(this.fileContextMenu_); 457 458 this.document_.addEventListener( 459 'canExecute', this.onRenameCanExecute_.bind(this)); 460 this.document_.addEventListener( 461 'canExecute', this.onDeleteCanExecute_.bind(this)); 462 463 this.document_.addEventListener('command', this.onCommand_.bind(this)); 464 } 465 466 /** 467 * One-time initialization of various DOM nodes. 468 */ 469 FileManager.prototype.initDom_ = function() { 470 // Cache nodes we'll be manipulating. 471 this.previewImage_ = this.dialogDom_.querySelector('.preview-img'); 472 this.previewFilename_ = this.dialogDom_.querySelector('.preview-filename'); 473 this.previewSummary_ = this.dialogDom_.querySelector('.preview-summary'); 474 this.filenameInput_ = this.dialogDom_.querySelector('.filename-input'); 475 this.taskButtons_ = this.dialogDom_.querySelector('.task-buttons'); 476 this.okButton_ = this.dialogDom_.querySelector('.ok'); 477 this.cancelButton_ = this.dialogDom_.querySelector('.cancel'); 478 this.newFolderButton_ = this.dialogDom_.querySelector('.new-folder'); 479 480 this.renameInput_ = this.document_.createElement('input'); 481 this.renameInput_.className = 'rename'; 482 483 this.renameInput_.addEventListener( 484 'keydown', this.onRenameInputKeyDown_.bind(this)); 485 this.renameInput_.addEventListener( 486 'blur', this.onRenameInputBlur_.bind(this)); 487 488 this.filenameInput_.addEventListener( 489 'keyup', this.onFilenameInputKeyUp_.bind(this)); 490 this.filenameInput_.addEventListener( 491 'focus', this.onFilenameInputFocus_.bind(this)); 492 493 this.dialogDom_.addEventListener('keydown', this.onKeyDown_.bind(this)); 494 this.okButton_.addEventListener('click', this.onOk_.bind(this)); 495 this.cancelButton_.addEventListener('click', this.onCancel_.bind(this)); 496 497 this.dialogDom_.querySelector('button.new-folder').addEventListener( 498 'click', this.onNewFolderButtonClick_.bind(this)); 499 500 if (ENABLE_THUMBNAIL_VIEW) { 501 this.dialogDom_.querySelector('button.detail-view').addEventListener( 502 'click', this.onDetailViewButtonClick_.bind(this)); 503 this.dialogDom_.querySelector('button.thumbnail-view').addEventListener( 504 'click', this.onThumbnailViewButtonClick_.bind(this)); 505 } else { 506 this.dialogDom_.querySelector( 507 'button.detail-view').style.display = 'none'; 508 this.dialogDom_.querySelector( 509 'button.thumbnail-view').style.display = 'none'; 510 } 511 512 this.dialogDom_.ownerDocument.defaultView.addEventListener( 513 'resize', this.onResize_.bind(this)); 514 515 var ary = this.dialogDom_.querySelectorAll('[visibleif]'); 516 for (var i = 0; i < ary.length; i++) { 517 var expr = ary[i].getAttribute('visibleif'); 518 if (!eval(expr)) 519 ary[i].style.display = 'none'; 520 } 521 522 // Populate the static localized strings. 523 i18nTemplate.process(this.document_, localStrings.templateData); 524 525 // Always sharing the data model between the detail/thumb views confuses 526 // them. Instead we maintain this bogus data model, and hook it up to the 527 // view that is not in use. 528 this.emptyDataModel_ = new cr.ui.table.TableDataModel([]); 529 530 this.dataModel_ = new cr.ui.table.TableDataModel([]); 531 this.dataModel_.sort('name'); 532 this.dataModel_.addEventListener('sorted', 533 this.onDataModelSorted_.bind(this)); 534 this.dataModel_.prepareSort = this.prepareSort_.bind(this); 535 536 if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE || 537 this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FOLDER || 538 this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) { 539 this.selectionModelClass_ = cr.ui.table.TableSingleSelectionModel; 540 } else { 541 this.selectionModelClass_ = cr.ui.table.TableSelectionModel; 542 } 543 544 this.initTable_(); 545 this.initGrid_(); 546 547 this.setListType(FileManager.ListType.DETAIL); 548 549 this.onResize_(); 550 this.dialogDom_.style.opacity = '1'; 551 }; 552 553 /** 554 * Force the canExecute events to be dispatched. 555 */ 556 FileManager.prototype.updateCommands_ = function() { 557 this.commands_['rename'].canExecuteChange(); 558 this.commands_['delete'].canExecuteChange(); 559 }; 560 561 /** 562 * Invoked to decide whether the "rename" command can be executed. 563 */ 564 FileManager.prototype.onRenameCanExecute_ = function(event) { 565 event.canExecute = 566 (// Full page mode. 567 this.dialogType_ == FileManager.DialogType.FULL_PAGE && 568 // Rename not in progress. 569 !this.renameInput_.currentEntry && 570 // Not in root directory. 571 this.currentDirEntry_.fullPath != '/' && 572 // Not in media directory. 573 this.currentDirEntry_.fullPath != MEDIA_DIRECTORY && 574 // Only one file selected. 575 this.selection.totalCount == 1); 576 }; 577 578 /** 579 * Invoked to decide whether the "delete" command can be executed. 580 */ 581 FileManager.prototype.onDeleteCanExecute_ = function(event) { 582 event.canExecute = 583 (// Full page mode. 584 this.dialogType_ == FileManager.DialogType.FULL_PAGE && 585 // Rename not in progress. 586 !this.renameInput_.currentEntry && 587 // Not in root directory. 588 this.currentDirEntry_.fullPath != '/' && 589 // Not in media directory. 590 this.currentDirEntry_.fullPath != MEDIA_DIRECTORY); 591 }; 592 593 FileManager.prototype.setListType = function(type) { 594 if (type && type == this.listType_) 595 return; 596 597 if (type == FileManager.ListType.DETAIL) { 598 this.table_.dataModel = this.dataModel_; 599 this.table_.style.display = ''; 600 this.grid_.style.display = 'none'; 601 this.grid_.dataModel = this.emptyDataModel_; 602 this.currentList_ = this.table_; 603 this.dialogDom_.querySelector('button.detail-view').disabled = true; 604 this.dialogDom_.querySelector('button.thumbnail-view').disabled = false; 605 } else if (type == FileManager.ListType.THUMBNAIL) { 606 this.grid_.dataModel = this.dataModel_; 607 this.grid_.style.display = ''; 608 this.table_.style.display = 'none'; 609 this.table_.dataModel = this.emptyDataModel_; 610 this.currentList_ = this.grid_; 611 this.dialogDom_.querySelector('button.thumbnail-view').disabled = true; 612 this.dialogDom_.querySelector('button.detail-view').disabled = false; 613 } else { 614 throw new Error('Unknown list type: ' + type); 615 } 616 617 this.listType_ = type; 618 this.onResize_(); 619 }; 620 621 /** 622 * Initialize the file thumbnail grid. 623 */ 624 FileManager.prototype.initGrid_ = function() { 625 this.grid_ = this.dialogDom_.querySelector('.thumbnail-grid'); 626 cr.ui.Grid.decorate(this.grid_); 627 628 var self = this; 629 this.grid_.itemConstructor = function(entry) { 630 return self.renderThumbnail_(entry); 631 }; 632 633 this.grid_.selectionModel = new this.selectionModelClass_(); 634 635 this.grid_.addEventListener( 636 'dblclick', this.onDetailDoubleClick_.bind(this)); 637 this.grid_.selectionModel.addEventListener( 638 'change', this.onSelectionChanged_.bind(this)); 639 640 if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) { 641 cr.ui.contextMenuHandler.addContextMenuProperty(this.grid_); 642 this.grid_.contextMenu = this.fileContextMenu_; 643 } 644 645 this.grid_.addEventListener('mousedown', 646 this.onGridMouseDown_.bind(this)); 647 }; 648 649 /** 650 * Initialize the file list table. 651 */ 652 FileManager.prototype.initTable_ = function() { 653 var checkWidth = this.showCheckboxes_ ? 5 : 0; 654 655 var columns = [ 656 new cr.ui.table.TableColumn('cachedIconType_', '', 657 5.4 + checkWidth), 658 new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'), 659 64 - checkWidth), 660 new cr.ui.table.TableColumn('cachedSize_', 661 str('SIZE_COLUMN_LABEL'), 15.5), 662 new cr.ui.table.TableColumn('cachedMtime_', 663 str('DATE_COLUMN_LABEL'), 21) 664 ]; 665 666 columns[0].renderFunction = this.renderIconType_.bind(this); 667 columns[1].renderFunction = this.renderName_.bind(this); 668 columns[2].renderFunction = this.renderSize_.bind(this); 669 columns[3].renderFunction = this.renderDate_.bind(this); 670 671 this.table_ = this.dialogDom_.querySelector('.detail-table'); 672 cr.ui.Table.decorate(this.table_); 673 674 this.table_.selectionModel = new this.selectionModelClass_(); 675 this.table_.columnModel = new cr.ui.table.TableColumnModel(columns); 676 677 this.table_.addEventListener( 678 'dblclick', this.onDetailDoubleClick_.bind(this)); 679 this.table_.selectionModel.addEventListener( 680 'change', this.onSelectionChanged_.bind(this)); 681 682 if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) { 683 cr.ui.contextMenuHandler.addContextMenuProperty(this.table_); 684 this.table_.contextMenu = this.fileContextMenu_; 685 } 686 687 this.table_.addEventListener('mousedown', 688 this.onTableMouseDown_.bind(this)); 689 }; 690 691 /** 692 * Respond to a command being executed. 693 */ 694 FileManager.prototype.onCommand_ = function(event) { 695 switch (event.command.id) { 696 case 'rename': 697 var leadIndex = this.currentList_.selectionModel.leadIndex; 698 var li = this.currentList_.getListItemByIndex(leadIndex); 699 var label = li.querySelector('.filename-label'); 700 if (!label) { 701 console.warn('Unable to find label for rename of index: ' + 702 leadIndex); 703 return; 704 } 705 706 this.initiateRename_(label); 707 break; 708 709 case 'delete': 710 this.deleteEntries(this.selection.entries); 711 break; 712 } 713 }; 714 715 /** 716 * Respond to the back button. 717 */ 718 FileManager.prototype.onPopState_ = function(event) { 719 this.changeDirectory(event.state, false); 720 }; 721 722 /** 723 * Resize details and thumb views to fit the new window size. 724 */ 725 FileManager.prototype.onResize_ = function() { 726 this.table_.style.height = this.grid_.style.height = 727 this.grid_.parentNode.clientHeight + 'px'; 728 this.table_.style.width = this.grid_.style.width = 729 this.grid_.parentNode.clientWidth + 'px'; 730 731 this.table_.list_.style.width = this.table_.parentNode.clientWidth + 'px'; 732 this.table_.list_.style.height = (this.table_.clientHeight - 1 - 733 this.table_.header_.clientHeight) + 'px'; 734 735 if (this.listType_ == FileManager.ListType.THUMBNAIL) { 736 var self = this; 737 setTimeout(function () { 738 self.grid_.columns = 0; 739 self.grid_.redraw(); 740 }, 0); 741 } else { 742 this.currentList_.redraw(); 743 } 744 }; 745 746 /** 747 * Tweak the UI to become a particular kind of dialog, as determined by the 748 * dialog type parameter passed to the constructor. 749 */ 750 FileManager.prototype.initDialogType_ = function() { 751 var defaultTitle; 752 var okLabel = str('OPEN_LABEL'); 753 754 // Split the dirname from the basename. 755 var ary = this.defaultPath_.match(/^(.*?)(?:\/([^\/]+))?$/); 756 var defaultFolder; 757 var defaultTarget; 758 759 if (!ary) { 760 console.warn('Unable to split defaultPath: ' + defaultPath); 761 ary = []; 762 } 763 764 switch (this.dialogType_) { 765 case FileManager.DialogType.SELECT_FOLDER: 766 defaultTitle = str('SELECT_FOLDER_TITLE'); 767 defaultFolder = ary[1] || '/'; 768 defaultTarget = ary[2] || ''; 769 break; 770 771 case FileManager.DialogType.SELECT_OPEN_FILE: 772 defaultTitle = str('SELECT_OPEN_FILE_TITLE'); 773 defaultFolder = ary[1] || '/'; 774 defaultTarget = ''; 775 776 if (ary[2]) { 777 console.warn('Open should NOT have provided a default ' + 778 'filename: ' + ary[2]); 779 } 780 break; 781 782 case FileManager.DialogType.SELECT_OPEN_MULTI_FILE: 783 defaultTitle = str('SELECT_OPEN_MULTI_FILE_TITLE'); 784 defaultFolder = ary[1] || '/'; 785 defaultTarget = ''; 786 787 if (ary[2]) { 788 console.warn('Multi-open should NOT have provided a default ' + 789 'filename: ' + ary[2]); 790 } 791 break; 792 793 case FileManager.DialogType.SELECT_SAVEAS_FILE: 794 defaultTitle = str('SELECT_SAVEAS_FILE_TITLE'); 795 okLabel = str('SAVE_LABEL'); 796 797 defaultFolder = ary[1] || '/'; 798 defaultTarget = ary[2] || ''; 799 if (!defaultTarget) 800 console.warn('Save-as should have provided a default filename.'); 801 break; 802 803 case FileManager.DialogType.FULL_PAGE: 804 defaultFolder = ary[1] || '/'; 805 defaultTarget = ary[2] || ''; 806 break; 807 808 default: 809 throw new Error('Unknown dialog type: ' + this.dialogType_); 810 } 811 812 this.okButton_.textContent = okLabel; 813 814 dialogTitle = this.params_.title || defaultTitle; 815 this.dialogDom_.querySelector('.dialog-title').textContent = dialogTitle; 816 817 ary = defaultFolder.match(/^\/home\/[^\/]+\/user\/Downloads(\/.*)?$/); 818 if (ary) { 819 // Chrome will probably suggest the full path to Downloads, but 820 // we're working with 'virtual paths', so we have to translate. 821 // TODO(rginda): Maybe chrome should have suggested the correct place 822 // to begin with, but that would mean it would have to treat the 823 // file manager dialogs differently than the native ones. 824 defaultFolder = '/Downloads' + (ary[1] || ''); 825 } 826 827 this.defaultFolder_ = defaultFolder; 828 this.filenameInput_.value = defaultTarget; 829 }; 830 831 /** 832 * Cache necessary data before a sort happens. 833 * 834 * This is called by the table code before a sort happens, so that we can 835 * go fetch data for the sort field that we may not have yet. 836 */ 837 FileManager.prototype.prepareSort_ = function(field, callback) { 838 var cacheFunction; 839 840 if (field == 'cachedMtime_') { 841 cacheFunction = cacheEntryDate; 842 } else if (field == 'cachedSize_') { 843 cacheFunction = cacheEntrySize; 844 } else if (field == 'cachedIconType_') { 845 cacheFunction = cacheEntryIconType; 846 } else { 847 callback(); 848 return; 849 } 850 851 function checkCount() { 852 if (uncachedCount == 0) { 853 // Callback via a setTimeout so the sync/async semantics don't change 854 // based on whether or not the value is cached. 855 setTimeout(callback, 0); 856 } 857 } 858 859 var dataModel = this.dataModel_; 860 var uncachedCount = dataModel.length; 861 862 for (var i = uncachedCount - 1; i >= 0 ; i--) { 863 var entry = dataModel.item(i); 864 if (field in entry) { 865 uncachedCount--; 866 } else { 867 cacheFunction(entry, function() { 868 uncachedCount--; 869 checkCount(); 870 }); 871 } 872 } 873 874 checkCount(); 875 } 876 877 /** 878 * Render (and wire up) a checkbox to be used in either a detail or a 879 * thumbnail list item. 880 */ 881 FileManager.prototype.renderCheckbox_ = function(entry) { 882 var input = this.document_.createElement('input'); 883 input.setAttribute('type', 'checkbox'); 884 input.className = 'file-checkbox'; 885 input.addEventListener('mousedown', 886 this.onCheckboxMouseDownUp_.bind(this)); 887 input.addEventListener('mouseup', 888 this.onCheckboxMouseDownUp_.bind(this)); 889 input.addEventListener('click', 890 this.onCheckboxClick_.bind(this)); 891 892 if (this.selection && this.selection.entries.indexOf(entry) != -1) { 893 // Our DOM nodes get discarded as soon as we're scrolled out of view, 894 // so we have to make sure the check state is correct when we're brought 895 // back to life. 896 input.checked = true; 897 } 898 899 return input; 900 } 901 902 FileManager.prototype.renderThumbnail_ = function(entry) { 903 var li = this.document_.createElement('li'); 904 li.className = 'thumbnail-item'; 905 906 if (this.showCheckboxes_) 907 li.appendChild(this.renderCheckbox_(entry)); 908 909 var div = this.document_.createElement('div'); 910 div.className = 'img-container'; 911 li.appendChild(div); 912 913 var img = this.document_.createElement('img'); 914 this.getThumbnailURL(entry, function(type, url) { img.src = url }); 915 div.appendChild(img); 916 917 div = this.document_.createElement('div'); 918 div.className = 'filename-label'; 919 var labelText = entry.name; 920 if (this.currentDirEntry_.name == '') 921 labelText = this.getLabelForRootPath_(labelText); 922 923 div.textContent = labelText; 924 div.entry = entry; 925 926 li.appendChild(div); 927 928 cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR); 929 cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR); 930 return li; 931 } 932 933 /** 934 * Render the type column of the detail table. 935 * 936 * Invoked by cr.ui.Table when a file needs to be rendered. 937 * 938 * @param {Entry} entry The Entry object to render. 939 * @param {string} columnId The id of the column to be rendered. 940 * @param {cr.ui.Table} table The table doing the rendering. 941 */ 942 FileManager.prototype.renderIconType_ = function(entry, columnId, table) { 943 var div = this.document_.createElement('div'); 944 div.className = 'detail-icon-container'; 945 946 if (this.showCheckboxes_) 947 div.appendChild(this.renderCheckbox_(entry)); 948 949 var icon = this.document_.createElement('div'); 950 icon.className = 'detail-icon'; 951 entry.cachedIconType_ = getIconType(entry); 952 icon.setAttribute('iconType', entry.cachedIconType_); 953 div.appendChild(icon); 954 955 return div; 956 }; 957 958 FileManager.prototype.getLabelForRootPath_ = function(path) { 959 // This hack lets us localize the top level directories. 960 if (path == 'Downloads') 961 return str('DOWNLOADS_DIRECTORY_LABEL'); 962 963 if (path == 'media') 964 return str('MEDIA_DIRECTORY_LABEL'); 965 966 return path || str('ROOT_DIRECTORY_LABEL'); 967 }; 968 969 /** 970 * Render the Name column of the detail table. 971 * 972 * Invoked by cr.ui.Table when a file needs to be rendered. 973 * 974 * @param {Entry} entry The Entry object to render. 975 * @param {string} columnId The id of the column to be rendered. 976 * @param {cr.ui.Table} table The table doing the rendering. 977 */ 978 FileManager.prototype.renderName_ = function(entry, columnId, table) { 979 var label = this.document_.createElement('div'); 980 label.entry = entry; 981 label.className = 'filename-label'; 982 if (this.currentDirEntry_.name == '') { 983 label.textContent = this.getLabelForRootPath_(entry.name); 984 } else { 985 label.textContent = entry.name; 986 } 987 988 return label; 989 }; 990 991 /** 992 * Render the Size column of the detail table. 993 * 994 * @param {Entry} entry The Entry object to render. 995 * @param {string} columnId The id of the column to be rendered. 996 * @param {cr.ui.Table} table The table doing the rendering. 997 */ 998 FileManager.prototype.renderSize_ = function(entry, columnId, table) { 999 var div = this.document_.createElement('div'); 1000 div.className = 'detail-size'; 1001 1002 div.textContent = '...'; 1003 cacheEntrySize(entry, function(entry) { 1004 if (entry.cachedSize_ == -1) { 1005 div.textContent = ''; 1006 } else { 1007 div.textContent = cr.locale.bytesToSi(entry.cachedSize_); 1008 } 1009 }); 1010 1011 return div; 1012 }; 1013 1014 /** 1015 * Render the Date column of the detail table. 1016 * 1017 * @param {Entry} entry The Entry object to render. 1018 * @param {string} columnId The id of the column to be rendered. 1019 * @param {cr.ui.Table} table The table doing the rendering. 1020 */ 1021 FileManager.prototype.renderDate_ = function(entry, columnId, table) { 1022 var div = this.document_.createElement('div'); 1023 div.className = 'detail-date'; 1024 1025 div.textContent = '...'; 1026 1027 var self = this; 1028 cacheEntryDate(entry, function(entry) { 1029 if (self.currentDirEntry_.fullPath == MEDIA_DIRECTORY && 1030 entry.cachedMtime_.getTime() == 0) { 1031 // Mount points for FAT volumes have this time associated with them. 1032 // We'd rather display nothing than this bogus date. 1033 div.textContent = '---'; 1034 } else { 1035 div.textContent = cr.locale.formatDate(entry.cachedMtime_, 1036 str('LOCALE_FMT_DATE_SHORT')); 1037 } 1038 }); 1039 1040 return div; 1041 }; 1042 1043 /** 1044 * Compute summary information about the current selection. 1045 * 1046 * This method dispatches the 'selection-summarized' event when it completes. 1047 * Depending on how many of the selected files already have known sizes, the 1048 * dispatch may happen immediately, or after a number of async calls complete. 1049 */ 1050 FileManager.prototype.summarizeSelection_ = function() { 1051 var selection = this.selection = { 1052 entries: [], 1053 urls: [], 1054 leadEntry: null, 1055 totalCount: 0, 1056 fileCount: 0, 1057 directoryCount: 0, 1058 bytes: 0, 1059 iconType: null, 1060 indexes: this.currentList_.selectionModel.selectedIndexes 1061 }; 1062 1063 this.previewSummary_.textContent = str('COMPUTING_SELECTION'); 1064 this.taskButtons_.innerHTML = ''; 1065 1066 if (!selection.indexes.length) { 1067 cr.dispatchSimpleEvent(this, 'selection-summarized'); 1068 return; 1069 } 1070 1071 var fileCount = 0; 1072 var byteCount = 0; 1073 var pendingFiles = []; 1074 1075 for (var i = 0; i < selection.indexes.length; i++) { 1076 var entry = this.dataModel_.item(selection.indexes[i]); 1077 1078 selection.entries.push(entry); 1079 selection.urls.push(entry.toURL()); 1080 1081 if (selection.iconType == null) { 1082 selection.iconType = getIconType(entry); 1083 } else if (selection.iconType != 'unknown') { 1084 var iconType = getIconType(entry); 1085 if (selection.iconType != iconType) 1086 selection.iconType = 'unknown'; 1087 } 1088 1089 selection.totalCount++; 1090 1091 if (entry.isFile) { 1092 if (!('cachedSize_' in entry)) { 1093 // Any file that hasn't been rendered may be missing its cachedSize_ 1094 // property. For example, visit a large file list, and press ctrl-a 1095 // to select all. In this case, we need to asynchronously get the 1096 // sizes for these files before telling the world the selection has 1097 // been summarized. See the 'computeNextFile' logic below. 1098 pendingFiles.push(entry); 1099 continue; 1100 } else { 1101 selection.bytes += entry.cachedSize_; 1102 } 1103 selection.fileCount += 1; 1104 } else { 1105 selection.directoryCount += 1; 1106 } 1107 } 1108 1109 var leadIndex = this.currentList_.selectionModel.leadIndex; 1110 if (leadIndex > -1) { 1111 selection.leadEntry = this.dataModel_.item(leadIndex); 1112 } else { 1113 selection.leadEntry = selection.entries[0]; 1114 } 1115 1116 var self = this; 1117 1118 function cacheNextFile(fileEntry) { 1119 if (fileEntry) { 1120 // We're careful to modify the 'selection', rather than 'self.selection' 1121 // here, just in case the selection has changed since this summarization 1122 // began. 1123 selection.bytes += fileEntry.cachedSize_; 1124 } 1125 1126 if (pendingFiles.length) { 1127 cacheEntrySize(pendingFiles.pop(), cacheNextFile); 1128 } else { 1129 self.dispatchEvent(new cr.Event('selection-summarized')); 1130 } 1131 }; 1132 1133 if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) { 1134 chrome.fileBrowserPrivate.getFileTasks(selection.urls, 1135 this.onTasksFound_.bind(this)); 1136 } 1137 1138 cacheNextFile(); 1139 }; 1140 1141 FileManager.prototype.onExifGiven_ = function(fileURL, metadata) { 1142 var observers = this.exifCache_[fileURL]; 1143 if (!observers || !(observers instanceof Array)) { 1144 console.error('Missing or invalid exif observers: ' + fileURL + ': ' + 1145 observers); 1146 return; 1147 } 1148 1149 for (var i = 0; i < observers.length; i++) { 1150 observers[i](metadata); 1151 } 1152 1153 this.exifCache_[fileURL] = metadata; 1154 }; 1155 1156 FileManager.prototype.onExifError_ = function(fileURL, step, error) { 1157 console.warn('Exif error: ' + fileURL + ': ' + step + ': ' + error); 1158 this.onExifGiven_(fileURL, {}); 1159 }; 1160 1161 FileManager.prototype.onExifReaderMessage_ = function(event) { 1162 var data = event.data; 1163 var self = this; 1164 1165 function fwd(methodName, args) { self[methodName].apply(self, args) }; 1166 1167 switch (data.verb) { 1168 case 'log': 1169 console.log.apply(console, ['exif:'].concat(data.arguments)); 1170 break; 1171 1172 case 'give-exif': 1173 fwd('onExifGiven_', data.arguments); 1174 break; 1175 1176 case 'give-exif-error': 1177 fwd('onExifError_', data.arguments); 1178 break; 1179 1180 default: 1181 console.log('Unknown message from exif reader: ' + data.verb, data); 1182 break; 1183 } 1184 }; 1185 1186 FileManager.prototype.onTasksFound_ = function(tasksList) { 1187 this.taskButtons_.innerHTML = ''; 1188 for (var i = 0; i < tasksList.length; i++) { 1189 var task = tasksList[i]; 1190 1191 // Tweak images, titles of internal tasks. 1192 var task_parts = task.taskId.split('|'); 1193 if (task_parts[0] == this.getExtensionId_()) { 1194 if (task_parts[1] == 'preview') { 1195 // TODO(serya): This hack needed until task.iconUrl get working 1196 // (see GetFileTasksFileBrowserFunction::RunImpl). 1197 task.iconUrl = 1198 chrome.extension.getURL('images/icon_preview_16x16.png'); 1199 task.title = str('PREVIEW_IMAGE'); 1200 } else if (task_parts[1] == 'play') { 1201 task.iconUrl = 1202 chrome.extension.getURL('images/icon_play_16x16.png'); 1203 task.title = str('PLAY_MEDIA').replace("&", ""); 1204 } else if (task_parts[1] == 'enqueue') { 1205 task.iconUrl = 1206 chrome.extension.getURL('images/icon_add_to_queue_16x16.png'); 1207 task.title = str('ENQUEUE'); 1208 } 1209 } 1210 1211 var button = this.document_.createElement('button'); 1212 button.addEventListener('click', this.onTaskButtonClicked_.bind(this)); 1213 button.className = 'task-button'; 1214 button.task = task; 1215 1216 var img = this.document_.createElement('img'); 1217 img.src = task.iconUrl; 1218 1219 button.appendChild(img); 1220 button.appendChild(this.document_.createTextNode(task.title)); 1221 1222 this.taskButtons_.appendChild(button); 1223 } 1224 }; 1225 1226 FileManager.prototype.getExtensionId_ = function() { 1227 return chrome.extension.getURL('').split('/')[2]; 1228 }; 1229 1230 FileManager.prototype.onTaskButtonClicked_ = function(event) { 1231 // Check internal tasks first. 1232 var task_parts = event.srcElement.task.taskId.split('|'); 1233 if (task_parts[0] == this.getExtensionId_()) { 1234 if (task_parts[1] == 'preview') { 1235 g_slideshow_data = this.selection.urls; 1236 chrome.tabs.create({url: "slideshow.html"}); 1237 } else if (task_parts[1] == 'play') { 1238 chrome.fileBrowserPrivate.viewFiles(this.selection.urls, 1239 event.srcElement.task.taskId); 1240 } else if (task_parts[1] == 'enqueue') { 1241 chrome.fileBrowserPrivate.viewFiles(this.selection.urls, 1242 event.srcElement.task.taskId); 1243 } 1244 return; 1245 } 1246 1247 chrome.fileBrowserPrivate.executeTask(event.srcElement.task.taskId, 1248 this.selection.urls); 1249 } 1250 1251 /** 1252 * Update the breadcrumb display to reflect the current directory. 1253 */ 1254 FileManager.prototype.updateBreadcrumbs_ = function() { 1255 var bc = this.dialogDom_.querySelector('.breadcrumbs'); 1256 bc.innerHTML = ''; 1257 1258 var fullPath = this.currentDirEntry_.fullPath.replace(/\/$/, ''); 1259 var pathNames = fullPath.split('/'); 1260 var path = ''; 1261 1262 for (var i = 0; i < pathNames.length; i++) { 1263 var pathName = pathNames[i]; 1264 path += pathName + '/'; 1265 1266 var div = this.document_.createElement('div'); 1267 div.className = 'breadcrumb-path'; 1268 if (i <= 1) { 1269 // i == 0: root directory itself, i == 1: the files it contains. 1270 div.textContent = this.getLabelForRootPath_(pathName); 1271 } else { 1272 div.textContent = pathName; 1273 } 1274 1275 div.path = path; 1276 div.addEventListener('click', this.onBreadcrumbClick_.bind(this)); 1277 1278 bc.appendChild(div); 1279 1280 if (i == pathNames.length - 1) { 1281 div.classList.add('breadcrumb-last'); 1282 } else { 1283 var spacer = this.document_.createElement('div'); 1284 spacer.className = 'breadcrumb-spacer'; 1285 spacer.textContent = RIGHT_TRIANGLE; 1286 bc.appendChild(spacer); 1287 } 1288 } 1289 }; 1290 1291 /** 1292 * Update the preview panel to display a given entry. 1293 * 1294 * The selection summary line is handled by the onSelectionSummarized handler 1295 * rather than this function, because summarization may not complete quickly. 1296 */ 1297 FileManager.prototype.updatePreview_ = function() { 1298 // Clear the preview image first, in case the thumbnail takes long to load. 1299 this.previewImage_.src = ''; 1300 // The transparent-background class is used to display the checkerboard 1301 // background for image thumbnails. We don't want to display it for 1302 // non-thumbnail preview images. 1303 this.previewImage_.classList.remove('transparent-background'); 1304 // The multiple-selected class indicates that more than one entry is 1305 // selcted. 1306 this.previewImage_.classList.remove('multiple-selected'); 1307 1308 if (!this.selection.totalCount) { 1309 this.previewFilename_.textContent = ''; 1310 return; 1311 } 1312 1313 var previewName = this.selection.leadEntry.name; 1314 if (this.currentDirEntry_.name == '') 1315 previewName = this.getLabelForRootPath_(previewName); 1316 1317 this.previewFilename_.textContent = previewName; 1318 1319 var iconType = getIconType(this.selection.leadEntry); 1320 if (iconType == 'image') { 1321 if (fileManager.selection.totalCount > 1) 1322 this.previewImage_.classList.add('multiple-selected'); 1323 } 1324 1325 var self = this; 1326 var leadEntry = this.selection.leadEntry; 1327 1328 this.getThumbnailURL(this.selection.leadEntry, function(iconType, url) { 1329 if (self.selection.leadEntry != leadEntry) { 1330 // Selection has changed since we asked, nevermind. 1331 return; 1332 } 1333 1334 if (url) { 1335 self.previewImage_.src = url; 1336 if (iconType == 'image') 1337 self.previewImage_.classList.add('transparent-background'); 1338 } else { 1339 self.previewImage_.src = previewArt['unknown']; 1340 } 1341 }); 1342 }; 1343 1344 FileManager.prototype.cacheExifMetadata_ = function(entry, callback) { 1345 var url = entry.toURL(); 1346 var cacheValue = this.exifCache_[url]; 1347 1348 if (!cacheValue) { 1349 // This is the first time anyone's asked, go get it. 1350 this.exifCache_[url] = [callback]; 1351 this.exifReader.postMessage({verb: 'get-exif', 1352 arguments: [entry.toURL()]}); 1353 return; 1354 } 1355 1356 if (cacheValue instanceof Array) { 1357 // Something is already pending, add to the list of observers. 1358 cacheValue.push(callback); 1359 return; 1360 } 1361 1362 if (cacheValue instanceof Object) { 1363 // We already know the answer, let the caller know in a fresh call stack. 1364 setTimeout(function() { callback(cacheValue) }); 1365 return; 1366 } 1367 1368 console.error('Unexpected exif cache value:' + cacheValue); 1369 }; 1370 1371 FileManager.prototype.getThumbnailURL = function(entry, callback) { 1372 if (!entry) 1373 return; 1374 1375 var iconType = getIconType(entry); 1376 if (iconType != 'image') { 1377 // Not an image, display a canned clip-art graphic. 1378 if (!(iconType in previewArt)) 1379 iconType = 'unknown'; 1380 1381 setTimeout(function() { callback(iconType, previewArt[iconType]) }); 1382 return; 1383 } 1384 1385 if (ENABLE_EXIF_READER) { 1386 if (entry.name.match(/\.jpe?g$/i)) { 1387 // File is a jpg image, fetch the exif thumbnail. 1388 this.cacheExifMetadata_(entry, function(metadata) { 1389 callback(iconType, metadata.thumbnailURL || entry.toURL()); 1390 }); 1391 return; 1392 } 1393 } 1394 1395 // File is some other kind of image, just return the url to the whole 1396 // thing. 1397 setTimeout(function() { callback(iconType, entry.toURL()) }); 1398 }; 1399 1400 /** 1401 * Change the current directory. 1402 * 1403 * Dispatches the 'directory-changed' event when the directory is successfully 1404 * changed. 1405 * 1406 * @param {string} path The absolute path to the new directory. 1407 * @param {bool} opt_saveHistory Save this in the history stack (defaults 1408 * to true). 1409 */ 1410 FileManager.prototype.changeDirectory = function(path, opt_saveHistory) { 1411 var self = this; 1412 1413 if (arguments.length == 1) { 1414 opt_saveHistory = true; 1415 } else { 1416 opt_saveHistory = !!opt_saveHistory; 1417 } 1418 1419 function onPathFound(dirEntry) { 1420 if (self.currentDirEntry_ && 1421 self.currentDirEntry_.fullPath == dirEntry.fullPath) { 1422 // Directory didn't actually change. 1423 return; 1424 } 1425 1426 var e = new cr.Event('directory-changed'); 1427 e.previousDirEntry = self.currentDirEntry_; 1428 e.newDirEntry = dirEntry; 1429 e.saveHistory = opt_saveHistory; 1430 self.currentDirEntry_ = dirEntry; 1431 self.dispatchEvent(e); 1432 }; 1433 1434 if (path == '/') 1435 return onPathFound(this.filesystem_.root); 1436 1437 this.filesystem_.root.getDirectory( 1438 path, {create: false}, onPathFound, 1439 function(err) { 1440 console.error('Error changing directory to: ' + path + ', ' + err); 1441 if (!self.currentDirEntry_) { 1442 // If we've never successfully changed to a directory, force them 1443 // to the root. 1444 self.changeDirectory('/'); 1445 } 1446 }); 1447 }; 1448 1449 FileManager.prototype.deleteEntries = function(entries) { 1450 if (!window.confirm(str('CONFIRM_DELETE'))) 1451 return; 1452 1453 var count = entries.length; 1454 1455 var self = this; 1456 function onDelete() { 1457 if (--count == 0) 1458 self.rescanDirectory_(); 1459 } 1460 1461 for (var i = 0; i < entries.length; i++) { 1462 var entry = entries[i]; 1463 if (entry.isFile) { 1464 entry.remove( 1465 onDelete, 1466 util.flog('Error deleting file: ' + entry.fullPath, onDelete)); 1467 } else { 1468 entry.removeRecursively( 1469 onDelete, 1470 util.flog('Error deleting folder: ' + entry.fullPath, onDelete)); 1471 } 1472 } 1473 }; 1474 1475 /** 1476 * Invoked by the table dataModel after a sort completes. 1477 * 1478 * We use this hook to make sure selected files stay visible after a sort. 1479 */ 1480 FileManager.prototype.onDataModelSorted_ = function() { 1481 var i = this.currentList_.selectionModel.leadIndex; 1482 this.currentList_.scrollIntoView(i); 1483 } 1484 1485 /** 1486 * Update the selection summary UI when the selection summarization completes. 1487 */ 1488 FileManager.prototype.onSelectionSummarized_ = function() { 1489 if (this.selection.totalCount == 0) { 1490 this.previewSummary_.textContent = str('NOTHING_SELECTED'); 1491 1492 } else if (this.selection.totalCount == 1) { 1493 this.previewSummary_.textContent = 1494 strf('ONE_FILE_SELECTED', cr.locale.bytesToSi(this.selection.bytes)); 1495 1496 } else { 1497 this.previewSummary_.textContent = 1498 strf('MANY_FILES_SELECTED', this.selection.totalCount, 1499 cr.locale.bytesToSi(this.selection.bytes)); 1500 } 1501 }; 1502 1503 /** 1504 * Handle a click event on a breadcrumb element. 1505 * 1506 * @param {Event} event The click event. 1507 */ 1508 FileManager.prototype.onBreadcrumbClick_ = function(event) { 1509 this.changeDirectory(event.srcElement.path); 1510 }; 1511 1512 FileManager.prototype.onCheckboxMouseDownUp_ = function(event) { 1513 // If exactly one file is selected and its checkbox is *not* clicked, 1514 // then this should be treated as a "normal" click (ie. the previous 1515 // selection should be cleared). 1516 if (this.selection.totalCount == 1 && this.selection.entries[0].isFile) { 1517 var selectedIndex = this.selection.indexes[0]; 1518 var listItem = this.currentList_.getListItemByIndex(selectedIndex); 1519 var checkbox = listItem.querySelector('input[type="checkbox"]'); 1520 if (!checkbox.checked) 1521 return; 1522 } 1523 1524 // Otherwise, treat clicking on a checkbox the same as a ctrl-click. 1525 // The default properties of event.ctrlKey make it read-only, but 1526 // don't prevent deletion, so we delete first, then set it true. 1527 delete event.ctrlKey; 1528 event.ctrlKey = true; 1529 }; 1530 1531 FileManager.prototype.onCheckboxClick_ = function(event) { 1532 if (event.shiftKey) { 1533 // Something about the timing of shift-clicks causes the checkbox 1534 // to get selected and then very quickly unselected. It appears that 1535 // we forcibly select the checkbox as part of onSelectionChanged, and 1536 // then the default action of this click event fires and toggles the 1537 // checkbox back off. 1538 // 1539 // Since we're going to force checkboxes into the correct state for any 1540 // multi-selection, we can prevent this shift click from toggling the 1541 // checkbox and avoid the trouble. 1542 event.preventDefault(); 1543 } 1544 }; 1545 1546 /** 1547 * Update the UI when the selection model changes. 1548 * 1549 * @param {cr.Event} event The change event. 1550 */ 1551 FileManager.prototype.onSelectionChanged_ = function(event) { 1552 var selectable; 1553 1554 this.summarizeSelection_(); 1555 this.updateOkButton_(); 1556 this.updatePreview_(); 1557 1558 var self = this; 1559 setTimeout(function() { self.onSelectionChangeComplete_(event) }, 0); 1560 }; 1561 1562 FileManager.prototype.onSelectionChangeComplete_ = function(event) { 1563 if (!this.showCheckboxes_) 1564 return; 1565 1566 for (var i = 0; i < event.changes.length; i++) { 1567 // Turn off any checkboxes for items that are no longer selected. 1568 var selectedIndex = event.changes[i].index; 1569 var listItem = this.currentList_.getListItemByIndex(selectedIndex); 1570 if (!listItem) { 1571 // When changing directories, we get notified about list items 1572 // that are no longer there. 1573 continue; 1574 } 1575 1576 if (!event.changes[i].selected) { 1577 var checkbox = listItem.querySelector('input[type="checkbox"]'); 1578 checkbox.checked = false; 1579 } 1580 } 1581 1582 if (this.selection.fileCount > 1) { 1583 // If more than one file is selected, make sure all checkboxes are lit 1584 // up. 1585 for (var i = 0; i < this.selection.entries.length; i++) { 1586 if (!this.selection.entries[i].isFile) 1587 continue; 1588 1589 var selectedIndex = this.selection.indexes[i]; 1590 var listItem = this.currentList_.getListItemByIndex(selectedIndex); 1591 if (listItem) 1592 listItem.querySelector('input[type="checkbox"]').checked = true; 1593 } 1594 } 1595 }; 1596 1597 FileManager.prototype.updateOkButton_ = function(event) { 1598 if (this.dialogType_ == FileManager.DialogType.SELECT_FOLDER) { 1599 selectable = this.selection.directoryCount == 1 && 1600 this.selection.fileCount == 0; 1601 } else if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE) { 1602 selectable = (this.selection.directoryCount == 0 && 1603 this.selection.fileCount == 1); 1604 } else if (this.dialogType_ == 1605 FileManager.DialogType.SELECT_OPEN_MULTI_FILE) { 1606 selectable = (this.selection.directoryCount == 0 && 1607 this.selection.fileCount >= 1); 1608 } else if (this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) { 1609 if (this.selection.leadEntry && this.selection.leadEntry.isFile) 1610 this.filenameInput_.value = this.selection.leadEntry.name; 1611 1612 if (this.currentDirEntry_.fullPath == '/' || 1613 this.currentDirEntry_.fullPath == MEDIA_DIRECTORY) { 1614 // Nothing can be saved in to the root or media/ directories. 1615 selectable = false; 1616 } else { 1617 selectable = !!this.filenameInput_.value; 1618 } 1619 } else if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) { 1620 // No "select" buttons on the full page UI. 1621 selectable = true; 1622 } else { 1623 throw new Error('Unknown dialog type'); 1624 } 1625 1626 this.okButton_.disabled = !selectable; 1627 }; 1628 1629 /** 1630 * Handle a double-click event on an entry in the detail list. 1631 * 1632 * @param {Event} event The click event. 1633 */ 1634 FileManager.prototype.onDetailDoubleClick_ = function(event) { 1635 if (this.renameInput_.currentEntry) { 1636 // Don't pay attention to double clicks during a rename. 1637 return; 1638 } 1639 1640 var i = this.currentList_.selectionModel.leadIndex; 1641 var entry = this.dataModel_.item(i); 1642 1643 if (entry.isDirectory) 1644 return this.changeDirectory(entry.fullPath); 1645 1646 if (!this.okButton_.disabled) 1647 this.onOk_(); 1648 1649 }; 1650 1651 /** 1652 * Update the UI when the current directory changes. 1653 * 1654 * @param {cr.Event} event The directory-changed event. 1655 */ 1656 FileManager.prototype.onDirectoryChanged_ = function(event) { 1657 if (event.saveHistory) { 1658 history.pushState(this.currentDirEntry_.fullPath, 1659 this.currentDirEntry_.fullPath, 1660 location.href); 1661 } 1662 1663 this.updateOkButton_(); 1664 // New folder should never be enabled in the root or media/ directories. 1665 this.newFolderButton_.disabled = 1666 (this.currentDirEntry_.fullPath == '/' || 1667 this.currentDirEntry_.fullPath == MEDIA_DIRECTORY); 1668 1669 this.document_.title = this.currentDirEntry_.fullPath; 1670 this.rescanDirectory_(); 1671 }; 1672 1673 /** 1674 * Update the UI when a disk is mounted or unmounted. 1675 * 1676 * @param {string} path The path that has been mounted or unmounted. 1677 */ 1678 FileManager.prototype.onDiskChanged_ = function(event) { 1679 if (event.eventType == 'added') { 1680 this.changeDirectory(event.volumeInfo.mountPath); 1681 } else if (event.eventType == 'removed') { 1682 if (this.currentDirEntry_ && 1683 isParentPath(event.volumeInfo.mountPath, 1684 this.currentDirEntry_.fullPath)) { 1685 this.changeDirectory(getParentPath(event.volumeInfo.mountPath)); 1686 } 1687 } 1688 }; 1689 1690 /** 1691 * Rescan the current directory, refreshing the list. 1692 * 1693 * @param {function()} opt_callback Optional function to invoke when the 1694 * rescan is complete. 1695 */ 1696 FileManager.prototype.rescanDirectory_ = function(opt_callback) { 1697 var self = this; 1698 var reader; 1699 1700 function onReadSome(entries) { 1701 if (entries.length == 0) { 1702 if (self.dataModel_.sortStatus.field != 'name') 1703 self.dataModel_.updateIndex(0); 1704 1705 if (opt_callback) 1706 opt_callback(); 1707 return; 1708 } 1709 1710 // Splice takes the to-be-spliced-in array as individual parameters, 1711 // rather than as an array, so we need to perform some acrobatics... 1712 var spliceArgs = [].slice.call(entries); 1713 1714 // Hide files that start with a dot ('.'). 1715 // TODO(rginda): User should be able to override this. Support for other 1716 // commonly hidden patterns might be nice too. 1717 if (self.filterFiles_) { 1718 spliceArgs = spliceArgs.filter(function(e) { 1719 return e.name.substr(0, 1) != '.'; 1720 }); 1721 } 1722 1723 spliceArgs.unshift(0, 0); // index, deleteCount 1724 self.dataModel_.splice.apply(self.dataModel_, spliceArgs); 1725 1726 // Keep reading until entries.length is 0. 1727 reader.readEntries(onReadSome); 1728 }; 1729 1730 this.lastLabelClick_ = null; 1731 1732 // Clear the table first. 1733 this.dataModel_.splice(0, this.dataModel_.length); 1734 1735 this.updateBreadcrumbs_(); 1736 1737 if (this.currentDirEntry_.fullPath != '/') { 1738 // If not the root directory, just read the contents. 1739 reader = this.currentDirEntry_.createReader(); 1740 reader.readEntries(onReadSome); 1741 return; 1742 } 1743 1744 // Otherwise, use the provided list of root subdirectories, since the 1745 // real local filesystem root directory (the one we use outside the 1746 // harness) can't be enumerated yet. 1747 var spliceArgs = [].slice.call(this.rootEntries_); 1748 spliceArgs.unshift(0, 0); // index, deleteCount 1749 self.dataModel_.splice.apply(self.dataModel_, spliceArgs); 1750 self.dataModel_.updateIndex(0); 1751 1752 if (opt_callback) 1753 opt_callback(); 1754 }; 1755 1756 FileManager.prototype.findListItem_ = function(event) { 1757 var node = event.srcElement; 1758 while (node) { 1759 if (node.tagName == 'LI') 1760 break; 1761 node = node.parentNode; 1762 } 1763 1764 return node; 1765 }; 1766 1767 FileManager.prototype.onGridMouseDown_ = function(event) { 1768 this.updateCommands_(); 1769 1770 if (this.allowRenameClick_(event, event.srcElement.parentNode)) { 1771 event.preventDefault(); 1772 this.initiateRename_(event.srcElement); 1773 } 1774 1775 if (event.button != 1) 1776 return; 1777 1778 var li = this.findListItem_(event); 1779 if (!li) 1780 return; 1781 }; 1782 1783 FileManager.prototype.onTableMouseDown_ = function(event) { 1784 this.updateCommands_(); 1785 1786 if (this.allowRenameClick_(event, 1787 event.srcElement.parentNode.parentNode)) { 1788 event.preventDefault(); 1789 this.initiateRename_(event.srcElement); 1790 } 1791 1792 if (event.button != 1) 1793 return; 1794 1795 var li = this.findListItem_(event); 1796 if (!li) { 1797 console.log('li not found', event); 1798 return; 1799 } 1800 }; 1801 1802 /** 1803 * Determine whether or not a click should initiate a rename. 1804 * 1805 * Renames can happen on mouse click if the user clicks on a label twice, 1806 * at least a half second apart. 1807 */ 1808 FileManager.prototype.allowRenameClick_ = function(event, row) { 1809 if (this.dialogType_ != FileManager.DialogType.FULL_PAGE || 1810 this.currentDirEntry_.name == '') { 1811 // Renaming only enabled for full-page mode, outside of the root 1812 // directory. 1813 return false; 1814 } 1815 1816 // Rename already in progress. 1817 if (this.renameInput_.currentEntry) 1818 return false; 1819 1820 // Didn't click on the label. 1821 if (event.srcElement.className != 'filename-label') 1822 return false; 1823 1824 // Wrong button or using a keyboard modifier. 1825 if (event.button != 0 || event.shiftKey || event.metaKey || event.altKey) { 1826 this.lastLabelClick_ = null; 1827 return false; 1828 } 1829 1830 var now = new Date(); 1831 1832 this.lastLabelClick_ = this.lastLabelClick_ || now; 1833 var delay = now - this.lastLabelClick_; 1834 if (!row.selected || delay < 500) 1835 return false; 1836 1837 this.lastLabelClick_ = now; 1838 return true; 1839 }; 1840 1841 FileManager.prototype.initiateRename_= function(label) { 1842 var input = this.renameInput_; 1843 1844 window.label = label; 1845 1846 input.value = label.textContent; 1847 input.style.top = label.offsetTop + 'px'; 1848 input.style.left = label.offsetLeft + 'px'; 1849 input.style.width = label.clientWidth + 'px'; 1850 label.parentNode.appendChild(input); 1851 input.focus(); 1852 var selectionEnd = input.value.lastIndexOf('.'); 1853 if (selectionEnd == -1) { 1854 input.select(); 1855 } else { 1856 input.selectionStart = 0; 1857 input.selectionEnd = selectionEnd; 1858 } 1859 1860 // This has to be set late in the process so we don't handle spurious 1861 // blur events. 1862 input.currentEntry = label.entry; 1863 }; 1864 1865 FileManager.prototype.onRenameInputKeyDown_ = function(event) { 1866 if (!this.renameInput_.currentEntry) 1867 return; 1868 1869 switch (event.keyCode) { 1870 case 27: // Escape 1871 this.cancelRename_(); 1872 event.preventDefault(); 1873 break; 1874 1875 case 13: // Enter 1876 this.commitRename_(); 1877 event.preventDefault(); 1878 break; 1879 } 1880 }; 1881 1882 FileManager.prototype.onRenameInputBlur_ = function(event) { 1883 if (this.renameInput_.currentEntry) 1884 this.cancelRename_(); 1885 }; 1886 1887 FileManager.prototype.commitRename_ = function() { 1888 var entry = this.renameInput_.currentEntry; 1889 var newName = this.renameInput_.value; 1890 1891 this.renameInput_.currentEntry = null; 1892 this.lastLabelClick_ = null; 1893 1894 if (this.renameInput_.parentNode) 1895 this.renameInput_.parentNode.removeChild(this.renameInput_); 1896 1897 var self = this; 1898 function onSuccess() { 1899 self.rescanDirectory_(function () { 1900 for (var i = 0; i < self.dataModel_.length; i++) { 1901 if (self.dataModel_.item(i).name == newName) { 1902 self.currentList_.selectionModel.selectedIndex = i; 1903 self.currentList_.scrollIndexIntoView(i); 1904 self.currentList_.focus(); 1905 return; 1906 } 1907 } 1908 }); 1909 } 1910 1911 function onError(err) { 1912 window.alert(strf('ERROR_RENAMING', entry.name, 1913 util.getFileErrorMnemonic(err.code))); 1914 } 1915 1916 entry.moveTo(this.currentDirEntry_, newName, onSuccess, onError); 1917 }; 1918 1919 FileManager.prototype.cancelRename_ = function(event) { 1920 this.renameInput_.currentEntry = null; 1921 this.lastLabelClick_ = null; 1922 1923 if (this.renameInput_.parentNode) 1924 this.renameInput_.parentNode.removeChild(this.renameInput_); 1925 }; 1926 1927 FileManager.prototype.onFilenameInputKeyUp_ = function(event) { 1928 this.okButton_.disabled = this.filenameInput_.value.length == 0; 1929 1930 if (event.keyCode == 13 /* Enter */ && !this.okButton_.disabled) 1931 this.onOk_(); 1932 }; 1933 1934 FileManager.prototype.onFilenameInputFocus_ = function(event) { 1935 var input = this.filenameInput_; 1936 1937 // On focus we want to select everything but the extension, but 1938 // Chrome will select-all after the focus event completes. We 1939 // schedule a timeout to alter the focus after that happens. 1940 setTimeout(function() { 1941 var selectionEnd = input.value.lastIndexOf('.'); 1942 if (selectionEnd == -1) { 1943 input.select(); 1944 } else { 1945 input.selectionStart = 0; 1946 input.selectionEnd = selectionEnd; 1947 } 1948 }, 0); 1949 }; 1950 1951 FileManager.prototype.onNewFolderButtonClick_ = function(event) { 1952 var name = ''; 1953 1954 while (1) { 1955 name = window.prompt(str('NEW_FOLDER_PROMPT'), name); 1956 if (!name) 1957 return; 1958 1959 if (name.indexOf('/') == -1) 1960 break; 1961 1962 alert(strf('ERROR_INVALID_FOLDER_CHARACTER', '/')); 1963 } 1964 1965 var self = this; 1966 1967 function onSuccess(dirEntry) { 1968 self.rescanDirectory_(function () { 1969 for (var i = 0; i < self.dataModel_.length; i++) { 1970 if (self.dataModel_.item(i).name == dirEntry.name) { 1971 self.currentList_.selectionModel.selectedIndex = i; 1972 self.currentList_.scrollIndexIntoView(i); 1973 self.currentList_.focus(); 1974 return; 1975 } 1976 } 1977 }); 1978 } 1979 1980 function onError(err) { 1981 window.alert(strf('ERROR_CREATING_FOLDER', name, 1982 util.getFileErrorMnemonic(err.code))); 1983 } 1984 1985 this.currentDirEntry_.getDirectory(name, {create: true, exclusive: true}, 1986 onSuccess, onError); 1987 }; 1988 1989 FileManager.prototype.onDetailViewButtonClick_ = function(event) { 1990 this.setListType(FileManager.ListType.DETAIL); 1991 }; 1992 1993 FileManager.prototype.onThumbnailViewButtonClick_ = function(event) { 1994 this.setListType(FileManager.ListType.THUMBNAIL); 1995 }; 1996 1997 FileManager.prototype.onKeyDown_ = function(event) { 1998 if (event.srcElement.tagName == 'INPUT') 1999 return; 2000 2001 switch (event.keyCode) { 2002 case 8: // Backspace => Up one directory. 2003 event.preventDefault(); 2004 var path = this.currentDirEntry_.fullPath; 2005 if (path && path != '/') { 2006 var path = path.replace(/\/[^\/]+$/, ''); 2007 this.changeDirectory(path || '/'); 2008 } 2009 break; 2010 2011 case 13: // Enter => Change directory or complete dialog. 2012 if (this.selection.totalCount == 1 && 2013 this.selection.leadEntry.isDirectory && 2014 this.dialogType_ != FileManager.SELECT_FOLDER) { 2015 this.changeDirectory(this.selection.leadEntry.fullPath); 2016 } else if (!this.okButton_.disabled) { 2017 this.onOk_(); 2018 } 2019 break; 2020 2021 case 32: // Ctrl-Space => New Folder. 2022 if (this.newFolderButton_.style.display != 'none' && event.ctrlKey) { 2023 event.preventDefault(); 2024 this.onNewFolderButtonClick_(); 2025 } 2026 break; 2027 2028 case 190: // Ctrl-. => Toggle filter files. 2029 if (event.ctrlKey) { 2030 this.filterFiles_ = !this.filterFiles_; 2031 this.rescanDirectory_(); 2032 } 2033 break; 2034 2035 case 46: // Delete. 2036 if (this.dialogType_ == FileManager.DialogType.FULL_PAGE && 2037 this.selection.totalCount > 0) { 2038 event.preventDefault(); 2039 this.deleteEntries(this.selection.entries); 2040 } 2041 break; 2042 } 2043 }; 2044 2045 /** 2046 * Handle a click of the cancel button. Closes the window. 2047 * 2048 * @param {Event} event The click event. 2049 */ 2050 FileManager.prototype.onCancel_ = function(event) { 2051 chrome.fileBrowserPrivate.cancelDialog(); 2052 }; 2053 2054 /** 2055 * Handle a click of the ok button. 2056 * 2057 * The ok button has different UI labels depending on the type of dialog, but 2058 * in code it's always referred to as 'ok'. 2059 * 2060 * @param {Event} event The click event. 2061 */ 2062 FileManager.prototype.onOk_ = function(event) { 2063 var currentDirUrl = this.currentDirEntry_.toURL(); 2064 2065 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/') 2066 currentDirUrl += '/'; 2067 2068 if (this.dialogType_ == FileManager.DialogType.SELECT_SAVEAS_FILE) { 2069 // Save-as doesn't require a valid selection from the list, since 2070 // we're going to take the filename from the text input. 2071 var filename = this.filenameInput_.value; 2072 if (!filename) 2073 throw new Error('Missing filename!'); 2074 2075 chrome.fileBrowserPrivate.selectFile(currentDirUrl + encodeURI(filename), 2076 0); 2077 // Window closed by above call. 2078 return; 2079 } 2080 2081 var ary = []; 2082 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes; 2083 2084 // All other dialog types require at least one selected list item. 2085 // The logic to control whether or not the ok button is enabled should 2086 // prevent us from ever getting here, but we sanity check to be sure. 2087 if (!selectedIndexes.length) 2088 throw new Error('Nothing selected!'); 2089 2090 for (var i = 0; i < selectedIndexes.length; i++) { 2091 var entry = this.dataModel_.item(selectedIndexes[i]); 2092 if (!entry) { 2093 console.log('Error locating selected file at index: ' + i); 2094 continue; 2095 } 2096 2097 ary.push(currentDirUrl + encodeURI(entry.name)); 2098 } 2099 2100 // Multi-file selection has no other restrictions. 2101 if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_MULTI_FILE) { 2102 chrome.fileBrowserPrivate.selectFiles(ary); 2103 // Window closed by above call. 2104 return; 2105 } 2106 2107 // In full screen mode, open all files for vieweing. 2108 if (this.dialogType_ == FileManager.DialogType.FULL_PAGE) { 2109 chrome.fileBrowserPrivate.viewFiles(ary, "default"); 2110 // Window stays open. 2111 return; 2112 } 2113 2114 // Everything else must have exactly one. 2115 if (ary.length > 1) 2116 throw new Error('Too many files selected!'); 2117 2118 if (this.dialogType_ == FileManager.DialogType.SELECT_FOLDER) { 2119 if (!this.selection.leadEntry.isDirectory) 2120 throw new Error('Selected entry is not a folder!'); 2121 } else if (this.dialogType_ == FileManager.DialogType.SELECT_OPEN_FILE) { 2122 if (!this.selection.leadEntry.isFile) 2123 throw new Error('Selected entry is not a file!'); 2124 } 2125 2126 chrome.fileBrowserPrivate.selectFile(ary[0], 0); 2127 // Window closed by above call. 2128 }; 2129 2130 })(); 2131