1 // Copyright (c) 2012 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 * FileManager constructor. 9 * 10 * FileManager objects encapsulate the functionality of the file selector 11 * dialogs, as well as the full screen file manager application (though the 12 * latter is not yet implemented). 13 * 14 * @constructor 15 */ 16 function FileManager() { 17 this.initializeQueue_ = new AsyncUtil.Group(); 18 19 /** 20 * Current list type. 21 * @type {ListType} 22 * @private 23 */ 24 this.listType_ = null; 25 26 /** 27 * Whether to suppress the focus moving or not. 28 * This is used to filter out focusing by mouse. 29 * @type {boolean} 30 * @private 31 */ 32 this.suppressFocus_ = false; 33 34 /** 35 * SelectionHandler. 36 * @type {SelectionHandler} 37 * @private 38 */ 39 this.selectionHandler_ = null; 40 } 41 42 /** 43 * Maximum delay in milliseconds for updating thumbnails in the bottom panel 44 * to mitigate flickering. If images load faster then the delay they replace 45 * old images smoothly. On the other hand we don't want to keep old images 46 * too long. 47 * 48 * @type {number} 49 * @const 50 */ 51 FileManager.THUMBNAIL_SHOW_DELAY = 100; 52 53 FileManager.prototype = { 54 __proto__: cr.EventTarget.prototype, 55 get directoryModel() { 56 return this.directoryModel_; 57 }, 58 get navigationList() { 59 return this.navigationList_; 60 }, 61 get document() { 62 return this.document_; 63 }, 64 get fileTransferController() { 65 return this.fileTransferController_; 66 }, 67 get backgroundPage() { 68 return this.backgroundPage_; 69 }, 70 get volumeManager() { 71 return this.volumeManager_; 72 } 73 }; 74 75 /** 76 * Unload the file manager. 77 * Used by background.js (when running in the packaged mode). 78 */ 79 function unload() { 80 fileManager.onBeforeUnload_(); 81 fileManager.onUnload_(); 82 } 83 84 /** 85 * List of dialog types. 86 * 87 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except 88 * FULL_PAGE which is specific to this code. 89 * 90 * @enum {string} 91 */ 92 var DialogType = { 93 SELECT_FOLDER: 'folder', 94 SELECT_UPLOAD_FOLDER: 'upload-folder', 95 SELECT_SAVEAS_FILE: 'saveas-file', 96 SELECT_OPEN_FILE: 'open-file', 97 SELECT_OPEN_MULTI_FILE: 'open-multi-file', 98 FULL_PAGE: 'full-page' 99 }; 100 101 /** 102 * @param {string} type Dialog type. 103 * @return {boolean} Whether the type is modal. 104 */ 105 DialogType.isModal = function(type) { 106 return type == DialogType.SELECT_FOLDER || 107 type == DialogType.SELECT_UPLOAD_FOLDER || 108 type == DialogType.SELECT_SAVEAS_FILE || 109 type == DialogType.SELECT_OPEN_FILE || 110 type == DialogType.SELECT_OPEN_MULTI_FILE; 111 }; 112 113 /** 114 * @param {string} type Dialog type. 115 * @return {boolean} Whether the type is open dialog. 116 */ 117 DialogType.isOpenDialog = function(type) { 118 return type == DialogType.SELECT_OPEN_FILE || 119 type == DialogType.SELECT_OPEN_MULTI_FILE; 120 }; 121 122 /** 123 * @param {string} type Dialog type. 124 * @return {boolean} Whether the type is folder selection dialog. 125 */ 126 DialogType.isFolderDialog = function(type) { 127 return type == DialogType.SELECT_FOLDER || 128 type == DialogType.SELECT_UPLOAD_FOLDER; 129 }; 130 131 /** 132 * Bottom margin of the list and tree for transparent preview panel. 133 * @const 134 */ 135 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52; 136 137 // Anonymous "namespace". 138 (function() { 139 140 // Private variables and helper functions. 141 142 /** 143 * Number of milliseconds in a day. 144 */ 145 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; 146 147 /** 148 * Some UI elements react on a single click and standard double click handling 149 * leads to confusing results. We ignore a second click if it comes soon 150 * after the first. 151 */ 152 var DOUBLE_CLICK_TIMEOUT = 200; 153 154 /** 155 * Update the element to display the information about remaining space for 156 * the storage. 157 * @param {!Element} spaceInnerBar Block element for a percentage bar 158 * representing the remaining space. 159 * @param {!Element} spaceInfoLabel Inline element to contain the message. 160 * @param {!Element} spaceOuterBar Block element around the percentage bar. 161 */ 162 var updateSpaceInfo = function( 163 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) { 164 spaceInnerBar.removeAttribute('pending'); 165 if (sizeStatsResult) { 166 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize); 167 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr); 168 169 var usedSpace = 170 sizeStatsResult.totalSize - sizeStatsResult.remainingSize; 171 spaceInnerBar.style.width = 172 (100 * usedSpace / sizeStatsResult.totalSize) + '%'; 173 174 spaceOuterBar.hidden = false; 175 } else { 176 spaceOuterBar.hidden = true; 177 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO'); 178 } 179 }; 180 181 // Public statics. 182 183 FileManager.ListType = { 184 DETAIL: 'detail', 185 THUMBNAIL: 'thumb' 186 }; 187 188 FileManager.prototype.initPreferences_ = function(callback) { 189 var group = new AsyncUtil.Group(); 190 191 // DRIVE preferences should be initialized before creating DirectoryModel 192 // to rebuild the roots list. 193 group.add(this.getPreferences_.bind(this)); 194 195 // Get startup preferences. 196 this.viewOptions_ = {}; 197 group.add(function(done) { 198 util.platform.getPreference(this.startupPrefName_, function(value) { 199 // Load the global default options. 200 try { 201 this.viewOptions_ = JSON.parse(value); 202 } catch (ignore) {} 203 // Override with window-specific options. 204 if (window.appState && window.appState.viewOptions) { 205 for (var key in window.appState.viewOptions) { 206 if (window.appState.viewOptions.hasOwnProperty(key)) 207 this.viewOptions_[key] = window.appState.viewOptions[key]; 208 } 209 } 210 done(); 211 }.bind(this)); 212 }.bind(this)); 213 214 // Get the command line option. 215 group.add(function(done) { 216 chrome.commandLinePrivate.hasSwitch( 217 'file-manager-show-checkboxes', function(flag) { 218 this.showCheckboxes_ = flag; 219 done(); 220 }.bind(this)); 221 }.bind(this)); 222 223 // TODO(yoshiki): Remove the flag when the feature is launched. 224 this.enableExperimentalWebstoreIntegration_ = true; 225 226 group.run(callback); 227 }; 228 229 /** 230 * One time initialization for the file system and related things. 231 * 232 * @param {function()} callback Completion callback. 233 * @private 234 */ 235 FileManager.prototype.initFileSystemUI_ = function(callback) { 236 this.table_.startBatchUpdates(); 237 this.grid_.startBatchUpdates(); 238 239 this.initFileList_(); 240 this.setupCurrentDirectory_(); 241 242 // PyAuto tests monitor this state by polling this variable 243 this.__defineGetter__('workerInitialized_', function() { 244 return this.metadataCache_.isInitialized(); 245 }.bind(this)); 246 247 this.initDateTimeFormatters_(); 248 249 var self = this; 250 251 // Get the 'allowRedeemOffers' preference before launching 252 // FileListBannerController. 253 this.getPreferences_(function(pref) { 254 /** @type {boolean} */ 255 var showOffers = pref['allowRedeemOffers']; 256 self.bannersController_ = new FileListBannerController( 257 self.directoryModel_, self.volumeManager_, self.document_, 258 showOffers); 259 self.bannersController_.addEventListener('relayout', 260 self.onResize_.bind(self)); 261 }); 262 263 var dm = this.directoryModel_; 264 dm.addEventListener('directory-changed', 265 this.onDirectoryChanged_.bind(this)); 266 dm.addEventListener('begin-update-files', function() { 267 self.currentList_.startBatchUpdates(); 268 }); 269 dm.addEventListener('end-update-files', function() { 270 self.restoreItemBeingRenamed_(); 271 self.currentList_.endBatchUpdates(); 272 }); 273 dm.addEventListener('scan-started', this.onScanStarted_.bind(this)); 274 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this)); 275 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this)); 276 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this)); 277 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this)); 278 dm.addEventListener('rescan-completed', 279 this.onRescanCompleted_.bind(this)); 280 281 this.directoryTree_.addEventListener('change', function() { 282 this.ensureDirectoryTreeItemNotBehindPreviewPanel_(); 283 }.bind(this)); 284 285 var stateChangeHandler = 286 this.onPreferencesChanged_.bind(this); 287 chrome.fileBrowserPrivate.onPreferencesChanged.addListener( 288 stateChangeHandler); 289 stateChangeHandler(); 290 291 var driveConnectionChangedHandler = 292 this.onDriveConnectionChanged_.bind(this); 293 this.volumeManager_.addEventListener('drive-connection-changed', 294 driveConnectionChangedHandler); 295 driveConnectionChangedHandler(); 296 297 // Set the initial focus. 298 this.refocus(); 299 // Set it as a fallback when there is no focus. 300 this.document_.addEventListener('focusout', function(e) { 301 setTimeout(function() { 302 // When there is no focus, the active element is the <body>. 303 if (this.document_.activeElement == this.document_.body) 304 this.refocus(); 305 }.bind(this), 0); 306 }.bind(this)); 307 308 this.initDataTransferOperations_(); 309 310 this.initContextMenus_(); 311 this.initCommands_(); 312 313 this.updateFileTypeFilter_(); 314 315 this.selectionHandler_.onFileSelectionChanged(); 316 317 this.table_.endBatchUpdates(); 318 this.grid_.endBatchUpdates(); 319 320 callback(); 321 }; 322 323 /** 324 * If |item| in the directory tree is behind the preview panel, scrolls up the 325 * parent view and make the item visible. This should be called when: 326 * - the selected item is changed in the directory tree. 327 * - the visibility of the the preview panel is changed. 328 * 329 * @private 330 */ 331 FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ = 332 function() { 333 var selectedSubTree = this.directoryTree_.selectedItem; 334 if (!selectedSubTree) 335 return; 336 var item = selectedSubTree.rowElement; 337 var parentView = this.directoryTree_; 338 339 var itemRect = item.getBoundingClientRect(); 340 if (!itemRect) 341 return; 342 343 var listRect = parentView.getBoundingClientRect(); 344 if (!listRect) 345 return; 346 347 var previewPanel = this.dialogDom_.querySelector('.preview-panel'); 348 var previewPanelRect = previewPanel.getBoundingClientRect(); 349 var panelHeight = previewPanelRect ? previewPanelRect.height : 0; 350 351 var itemBottom = itemRect.bottom; 352 var listBottom = listRect.bottom - panelHeight; 353 354 if (itemBottom > listBottom) { 355 var scrollOffset = itemBottom - listBottom; 356 parentView.scrollTop += scrollOffset; 357 } 358 }; 359 360 /** 361 * @private 362 */ 363 FileManager.prototype.initDateTimeFormatters_ = function() { 364 var use12hourClock = !this.preferences_['use24hourClock']; 365 this.table_.setDateTimeFormat(use12hourClock); 366 }; 367 368 /** 369 * @private 370 */ 371 FileManager.prototype.initDataTransferOperations_ = function() { 372 this.fileOperationManager_ = FileOperationManagerWrapper.getInstance( 373 this.backgroundPage_); 374 375 // CopyManager are required for 'Delete' operation in 376 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed. 377 if (this.dialogType != DialogType.FULL_PAGE) return; 378 379 // TODO(hidehiko): Extract FileOperationManager related code from 380 // FileManager to simplify it. 381 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this); 382 this.fileOperationManager_.addEventListener( 383 'copy-progress', this.onCopyProgressBound_); 384 385 this.onEntryChangedBound_ = this.onEntryChanged_.bind(this); 386 this.fileOperationManager_.addEventListener( 387 'entry-changed', this.onEntryChangedBound_); 388 389 var controller = this.fileTransferController_ = 390 new FileTransferController(this.document_, 391 this.fileOperationManager_, 392 this.metadataCache_, 393 this.directoryModel_); 394 controller.attachDragSource(this.table_.list); 395 controller.attachFileListDropTarget(this.table_.list); 396 controller.attachDragSource(this.grid_); 397 controller.attachFileListDropTarget(this.grid_); 398 controller.attachTreeDropTarget(this.directoryTree_); 399 controller.attachNavigationListDropTarget(this.navigationList_, true); 400 controller.attachCopyPasteHandlers(); 401 controller.addEventListener('selection-copied', 402 this.blinkSelection.bind(this)); 403 controller.addEventListener('selection-cut', 404 this.blinkSelection.bind(this)); 405 }; 406 407 /** 408 * One-time initialization of context menus. 409 * @private 410 */ 411 FileManager.prototype.initContextMenus_ = function() { 412 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu'); 413 cr.ui.Menu.decorate(this.fileContextMenu_); 414 415 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_); 416 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'), 417 this.fileContextMenu_); 418 cr.ui.contextMenuHandler.setContextMenu( 419 this.document_.querySelector('.drive-welcome.page'), 420 this.fileContextMenu_); 421 422 this.rootsContextMenu_ = 423 this.dialogDom_.querySelector('#roots-context-menu'); 424 cr.ui.Menu.decorate(this.rootsContextMenu_); 425 this.navigationList_.setContextMenu(this.rootsContextMenu_); 426 427 this.directoryTreeContextMenu_ = 428 this.dialogDom_.querySelector('#directory-tree-context-menu'); 429 cr.ui.Menu.decorate(this.directoryTreeContextMenu_); 430 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_; 431 432 this.textContextMenu_ = 433 this.dialogDom_.querySelector('#text-context-menu'); 434 cr.ui.Menu.decorate(this.textContextMenu_); 435 436 this.gearButton_ = this.dialogDom_.querySelector('#gear-button'); 437 this.gearButton_.addEventListener('menushow', 438 this.refreshRemainingSpace_.bind(this, 439 false /* Without loading caption. */)); 440 this.dialogDom_.querySelector('#gear-menu').menuItemSelector = 441 'menuitem, hr'; 442 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton); 443 444 if (this.dialogType == DialogType.FULL_PAGE) { 445 // This is to prevent the buttons from stealing focus on mouse down. 446 var preventFocus = function(event) { 447 event.preventDefault(); 448 }; 449 450 var maximizeButton = this.dialogDom_.querySelector('#maximize-button'); 451 maximizeButton.addEventListener('click', this.onMaximize.bind(this)); 452 maximizeButton.addEventListener('mousedown', preventFocus); 453 454 var closeButton = this.dialogDom_.querySelector('#close-button'); 455 closeButton.addEventListener('click', this.onClose.bind(this)); 456 closeButton.addEventListener('mousedown', preventFocus); 457 } 458 459 this.syncButton.checkable = true; 460 this.hostedButton.checkable = true; 461 this.detailViewButton_.checkable = true; 462 this.thumbnailViewButton_.checkable = true; 463 464 if (util.platform.runningInBrowser()) { 465 // Suppresses the default context menu. 466 this.dialogDom_.addEventListener('contextmenu', function(e) { 467 e.preventDefault(); 468 e.stopPropagation(); 469 }); 470 } 471 }; 472 473 FileManager.prototype.onMaximize = function() { 474 var appWindow = chrome.app.window.current(); 475 if (appWindow.isMaximized()) 476 appWindow.restore(); 477 else 478 appWindow.maximize(); 479 }; 480 481 FileManager.prototype.onClose = function() { 482 window.close(); 483 }; 484 485 /** 486 * One-time initialization of commands. 487 * @private 488 */ 489 FileManager.prototype.initCommands_ = function() { 490 this.commandHandler = new CommandHandler(this); 491 492 // TODO(hirono): Move the following block to the UI part. 493 var commandButtons = this.dialogDom_.querySelectorAll('button[command]'); 494 for (var j = 0; j < commandButtons.length; j++) 495 CommandButton.decorate(commandButtons[j]); 496 497 var inputs = this.dialogDom_.querySelectorAll( 498 'input[type=text], input[type=search], textarea'); 499 for (var i = 0; i < inputs.length; i++) { 500 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_); 501 this.registerInputCommands_(inputs[i]); 502 } 503 504 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_, 505 this.textContextMenu_); 506 this.registerInputCommands_(this.renameInput_); 507 this.document_.addEventListener('command', 508 this.setNoHover_.bind(this, true)); 509 }; 510 511 /** 512 * Registers cut, copy, paste and delete commands on input element. 513 * 514 * @param {Node} node Text input element to register on. 515 * @private 516 */ 517 FileManager.prototype.registerInputCommands_ = function(node) { 518 CommandUtil.forceDefaultHandler(node, 'cut'); 519 CommandUtil.forceDefaultHandler(node, 'copy'); 520 CommandUtil.forceDefaultHandler(node, 'paste'); 521 CommandUtil.forceDefaultHandler(node, 'delete'); 522 node.addEventListener('keydown', function(e) { 523 var key = util.getKeyModifiers(e) + e.keyCode; 524 if (key === '190' /* '/' */ || key === '191' /* '.' */) { 525 // If this key event is propagated, this is handled search command, 526 // which calls 'preventDefault' method. 527 e.stopPropagation(); 528 } 529 }); 530 }; 531 532 /** 533 * Entry point of the initialization. 534 * This method is called from main.js. 535 */ 536 FileManager.prototype.initializeCore = function() { 537 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral'); 538 this.initializeQueue_.add(this.initBackgroundPage_.bind(this), 539 [], 'initBackgroundPage'); 540 this.initializeQueue_.add(this.initPreferences_.bind(this), 541 ['initGeneral'], 'initPreferences'); 542 this.initializeQueue_.add(this.initVolumeManager_.bind(this), 543 ['initGeneral', 'initBackgroundPage'], 544 'initVolumeManager'); 545 546 this.initializeQueue_.run(); 547 }; 548 549 FileManager.prototype.initializeUI = function(dialogDom, callback) { 550 this.dialogDom_ = dialogDom; 551 this.document_ = this.dialogDom_.ownerDocument; 552 553 this.initializeQueue_.add( 554 this.initEssentialUI_.bind(this), 555 ['initGeneral', 'initBackgroundPage'], 556 'initEssentialUI'); 557 this.initializeQueue_.add(this.initAdditionalUI_.bind(this), 558 ['initEssentialUI'], 'initAdditionalUI'); 559 this.initializeQueue_.add( 560 this.initFileSystemUI_.bind(this), 561 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI'); 562 563 // Run again just in case if all pending closures have completed and the 564 // queue has stopped and monitor the completion. 565 this.initializeQueue_.run(callback); 566 }; 567 568 /** 569 * Initializes general purpose basic things, which are used by other 570 * initializing methods. 571 * 572 * @param {function()} callback Completion callback. 573 * @private 574 */ 575 FileManager.prototype.initGeneral_ = function(callback) { 576 // Initialize the application state. 577 if (window.appState) { 578 this.params_ = window.appState.params || {}; 579 this.defaultPath = window.appState.defaultPath; 580 } else { 581 this.params_ = location.search ? 582 JSON.parse(decodeURIComponent(location.search.substr(1))) : 583 {}; 584 this.defaultPath = this.params_.defaultPath; 585 } 586 587 // Initialize the member variables that depend this.params_. 588 this.dialogType = this.params_.type || DialogType.FULL_PAGE; 589 this.startupPrefName_ = 'file-manager-' + this.dialogType; 590 this.fileTypes_ = this.params_.typeList || []; 591 592 callback(); 593 }; 594 595 /** 596 * Initialize the background page. 597 * @param {function()} callback Completion callback. 598 * @private 599 */ 600 FileManager.prototype.initBackgroundPage_ = function(callback) { 601 chrome.runtime.getBackgroundPage(function(backgroundPage) { 602 this.backgroundPage_ = backgroundPage; 603 this.backgroundPage_.background.ready(function() { 604 loadTimeData.data = this.backgroundPage_.background.stringData; 605 callback(); 606 }.bind(this)); 607 }.bind(this)); 608 }; 609 610 /** 611 * Initializes the VolumeManager instance. 612 * @param {function()} callback Completion callback. 613 * @private 614 */ 615 FileManager.prototype.initVolumeManager_ = function(callback) { 616 // Auto resolving to local path does not work for folders (e.g., dialog for 617 // loading unpacked extensions). 618 var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type); 619 620 // If this condition is false, VolumeManagerWrapper hides all drive 621 // related event and data, even if Drive is enabled on preference. 622 // In other words, even if Drive is disabled on preference but Files.app 623 // should show Drive when it is re-enabled, then the value should be set to 624 // true. 625 // Note that the Drive enabling preference change is listened by 626 // DriveIntegrationService, so here we don't need to take care about it. 627 var driveEnabled = 628 !noLocalPathResolution || !this.params_.shouldReturnLocalPath; 629 this.volumeManager_ = new VolumeManagerWrapper( 630 driveEnabled, this.backgroundPage_); 631 callback(); 632 }; 633 634 /** 635 * One time initialization of the Files.app's essential UI elements. These 636 * elements will be shown to the user. Only visible elements should be 637 * initialized here. Any heavy operation should be avoided. Files.app's 638 * window is shown at the end of this routine. 639 * 640 * @param {function()} callback Completion callback. 641 * @private 642 */ 643 FileManager.prototype.initEssentialUI_ = function(callback) { 644 // Optional list of file types. 645 metrics.recordEnum('Create', this.dialogType, 646 [DialogType.SELECT_FOLDER, 647 DialogType.SELECT_UPLOAD_FOLDER, 648 DialogType.SELECT_SAVEAS_FILE, 649 DialogType.SELECT_OPEN_FILE, 650 DialogType.SELECT_OPEN_MULTI_FILE, 651 DialogType.FULL_PAGE]); 652 653 // Create the metadata cache. 654 this.metadataCache_ = MetadataCache.createFull(); 655 656 // Create the root view of FileManager. 657 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType); 658 this.fileTypeSelector_ = this.ui_.fileTypeSelector; 659 this.okButton_ = this.ui_.okButton; 660 this.cancelButton_ = this.ui_.cancelButton; 661 662 // Show the window as soon as the UI pre-initialization is done. 663 if (this.dialogType == DialogType.FULL_PAGE && 664 !util.platform.runningInBrowser()) { 665 chrome.app.window.current().show(); 666 setTimeout(callback, 100); // Wait until the animation is finished. 667 } else { 668 callback(); 669 } 670 }; 671 672 /** 673 * One-time initialization of dialogs. 674 * @private 675 */ 676 FileManager.prototype.initDialogs_ = function() { 677 // Initialize the dialog. 678 this.ui_.initDialogs(); 679 FileManagerDialogBase.setFileManager(this); 680 681 // Obtains the dialog instances from FileManagerUI. 682 // TODO(hirono): Remove the properties from the FileManager class. 683 this.error = this.ui_.errorDialog; 684 this.alert = this.ui_.alertDialog; 685 this.confirm = this.ui_.confirmDialog; 686 this.prompt = this.ui_.promptDialog; 687 this.shareDialog_ = this.ui_.shareDialog; 688 this.defaultTaskPicker = this.ui_.defaultTaskPicker; 689 this.suggestAppsDialog = this.ui_.suggestAppsDialog; 690 }; 691 692 /** 693 * One-time initialization of various DOM nodes. Loads the additional DOM 694 * elements visible to the user. Initialize here elements, which are expensive 695 * or hidden in the beginning. 696 * 697 * @param {function()} callback Completion callback. 698 * @private 699 */ 700 FileManager.prototype.initAdditionalUI_ = function(callback) { 701 this.initDialogs_(); 702 this.ui_.initAdditionalUI(); 703 704 this.dialogDom_.addEventListener('drop', function(e) { 705 // Prevent opening an URL by dropping it onto the page. 706 e.preventDefault(); 707 }); 708 709 this.dialogDom_.addEventListener('click', 710 this.onExternalLinkClick_.bind(this)); 711 // Cache nodes we'll be manipulating. 712 var dom = this.dialogDom_; 713 714 this.filenameInput_ = dom.querySelector('#filename-input-box input'); 715 this.taskItems_ = dom.querySelector('#tasks'); 716 717 this.table_ = dom.querySelector('.detail-table'); 718 this.grid_ = dom.querySelector('.thumbnail-grid'); 719 this.spinner_ = dom.querySelector('#list-container > .spinner-layer'); 720 this.showSpinner_(true); 721 722 // Check the option to hide the selecting checkboxes. 723 this.table_.showCheckboxes = this.showCheckboxes_; 724 725 var fullPage = this.dialogType == DialogType.FULL_PAGE; 726 FileTable.decorate(this.table_, this.metadataCache_, fullPage); 727 FileGrid.decorate(this.grid_, this.metadataCache_); 728 729 this.previewPanel_ = new PreviewPanel( 730 dom.querySelector('.preview-panel'), 731 DialogType.isOpenDialog(this.dialogType) ? 732 PreviewPanel.VisibilityType.ALWAYS_VISIBLE : 733 PreviewPanel.VisibilityType.AUTO, 734 this.metadataCache_, 735 this.volumeManager_); 736 this.previewPanel_.addEventListener( 737 PreviewPanel.Event.VISIBILITY_CHANGE, 738 this.onPreviewPanelVisibilityChange_.bind(this)); 739 this.previewPanel_.initialize(); 740 741 this.previewPanel_.breadcrumbs.addEventListener( 742 'pathclick', this.onBreadcrumbClick_.bind(this)); 743 744 // Initialize progress center panel. 745 this.progressCenterPanel_ = new ProgressCenterPanel( 746 dom.querySelector('#progress-center')); 747 this.backgroundPage_.background.progressCenter.addPanel( 748 this.progressCenterPanel_); 749 750 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this)); 751 752 // This capturing event is only used to distinguish focusing using 753 // keyboard from focusing using mouse. 754 this.document_.addEventListener('mousedown', function() { 755 this.suppressFocus_ = true; 756 }.bind(this), true); 757 758 this.renameInput_ = this.document_.createElement('input'); 759 this.renameInput_.className = 'rename'; 760 761 this.renameInput_.addEventListener( 762 'keydown', this.onRenameInputKeyDown_.bind(this)); 763 this.renameInput_.addEventListener( 764 'blur', this.onRenameInputBlur_.bind(this)); 765 766 // TODO(hirono): Rename the handler after creating the DialogFooter class. 767 this.filenameInput_.addEventListener( 768 'input', this.onFilenameInputInput_.bind(this)); 769 this.filenameInput_.addEventListener( 770 'keydown', this.onFilenameInputKeyDown_.bind(this)); 771 this.filenameInput_.addEventListener( 772 'focus', this.onFilenameInputFocus_.bind(this)); 773 774 this.listContainer_ = this.dialogDom_.querySelector('#list-container'); 775 this.listContainer_.addEventListener( 776 'keydown', this.onListKeyDown_.bind(this)); 777 this.listContainer_.addEventListener( 778 'keypress', this.onListKeyPress_.bind(this)); 779 this.listContainer_.addEventListener( 780 'mousemove', this.onListMouseMove_.bind(this)); 781 782 this.okButton_.addEventListener('click', this.onOk_.bind(this)); 783 this.onCancelBound_ = this.onCancel_.bind(this); 784 this.cancelButton_.addEventListener('click', this.onCancelBound_); 785 786 this.decorateSplitter( 787 this.dialogDom_.querySelector('#navigation-list-splitter')); 788 this.decorateSplitter( 789 this.dialogDom_.querySelector('#middlebar-splitter')); 790 791 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container'); 792 793 this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings'); 794 this.syncButton.addEventListener('click', this.onDrivePrefClick_.bind( 795 this, 'cellularDisabled', false /* not inverted */)); 796 797 this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings'); 798 this.hostedButton.addEventListener('click', this.onDrivePrefClick_.bind( 799 this, 'hostedFilesDisabled', true /* inverted */)); 800 801 this.detailViewButton_ = 802 this.dialogDom_.querySelector('#detail-view'); 803 this.detailViewButton_.addEventListener('activate', 804 this.onDetailViewButtonClick_.bind(this)); 805 806 this.thumbnailViewButton_ = 807 this.dialogDom_.querySelector('#thumbnail-view'); 808 this.thumbnailViewButton_.addEventListener('activate', 809 this.onThumbnailViewButtonClick_.bind(this)); 810 811 cr.ui.ComboButton.decorate(this.taskItems_); 812 this.taskItems_.showMenu = function(shouldSetFocus) { 813 // Prevent the empty menu from opening. 814 if (!this.menu.length) 815 return; 816 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus); 817 }; 818 this.taskItems_.addEventListener('select', 819 this.onTaskItemClicked_.bind(this)); 820 821 this.dialogDom_.ownerDocument.defaultView.addEventListener( 822 'resize', this.onResize_.bind(this)); 823 824 this.filePopup_ = null; 825 826 this.searchBoxWrapper_ = this.ui_.searchBox.element; 827 this.searchBox_ = this.ui_.searchBox.inputElement; 828 this.searchBox_.addEventListener( 829 'input', this.onSearchBoxUpdate_.bind(this)); 830 this.ui_.searchBox.clearButton.addEventListener( 831 'click', this.onSearchClearButtonClick_.bind(this)); 832 833 this.lastSearchQuery_ = ''; 834 835 this.autocompleteList_ = this.ui_.searchBox.autocompleteList; 836 this.autocompleteList_.requestSuggestions = 837 this.requestAutocompleteSuggestions_.bind(this); 838 839 // Instead, open the suggested item when Enter key is pressed or 840 // mouse-clicked. 841 this.autocompleteList_.handleEnterKeydown = function(event) { 842 this.openAutocompleteSuggestion_(); 843 this.lastAutocompleteQuery_ = ''; 844 this.autocompleteList_.suggestions = []; 845 }.bind(this); 846 this.autocompleteList_.addEventListener('mousedown', function(event) { 847 this.openAutocompleteSuggestion_(); 848 this.lastAutocompleteQuery_ = ''; 849 this.autocompleteList_.suggestions = []; 850 }.bind(this)); 851 852 this.defaultActionMenuItem_ = 853 this.dialogDom_.querySelector('#default-action'); 854 855 this.openWithCommand_ = 856 this.dialogDom_.querySelector('#open-with'); 857 858 this.driveBuyMoreStorageCommand_ = 859 this.dialogDom_.querySelector('#drive-buy-more-space'); 860 861 this.defaultActionMenuItem_.addEventListener('click', 862 this.dispatchSelectionAction_.bind(this)); 863 864 this.initFileTypeFilter_(); 865 866 util.addIsFocusedMethod(); 867 868 // Populate the static localized strings. 869 i18nTemplate.process(this.document_, loadTimeData); 870 871 // Arrange the file list. 872 this.table_.normalizeColumns(); 873 this.table_.redraw(); 874 875 callback(); 876 }; 877 878 /** 879 * @private 880 */ 881 FileManager.prototype.onBreadcrumbClick_ = function(event) { 882 // TODO(hirono): Use directoryModel#changeDirectoryEntry after implementing 883 // it. 884 if (event.entry === RootType.DRIVE_SHARED_WITH_ME) 885 this.directoryModel_.changeDirectory(RootDirectory.DRIVE_SHARED_WITH_ME); 886 else 887 this.directoryModel_.changeDirectory(event.entry.fullPath); 888 }; 889 890 /** 891 * Constructs table and grid (heavy operation). 892 * @private 893 **/ 894 FileManager.prototype.initFileList_ = function() { 895 // Always sharing the data model between the detail/thumb views confuses 896 // them. Instead we maintain this bogus data model, and hook it up to the 897 // view that is not in use. 898 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]); 899 this.emptySelectionModel_ = new cr.ui.ListSelectionModel(); 900 901 var singleSelection = 902 this.dialogType == DialogType.SELECT_OPEN_FILE || 903 this.dialogType == DialogType.SELECT_FOLDER || 904 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER || 905 this.dialogType == DialogType.SELECT_SAVEAS_FILE; 906 907 this.fileFilter_ = new FileFilter( 908 this.metadataCache_, 909 false /* Don't show dot files by default. */); 910 911 this.fileWatcher_ = new FileWatcher(this.metadataCache_); 912 this.fileWatcher_.addEventListener( 913 'watcher-metadata-changed', 914 this.onWatcherMetadataChanged_.bind(this)); 915 916 this.directoryModel_ = new DirectoryModel( 917 singleSelection, 918 this.fileFilter_, 919 this.fileWatcher_, 920 this.metadataCache_, 921 this.volumeManager_); 922 923 this.folderShortcutsModel_ = new FolderShortcutsDataModel(); 924 925 this.selectionHandler_ = new FileSelectionHandler(this); 926 927 var dataModel = this.directoryModel_.getFileList(); 928 929 this.table_.setupCompareFunctions(dataModel); 930 931 dataModel.addEventListener('permuted', 932 this.updateStartupPrefs_.bind(this)); 933 934 this.directoryModel_.getFileListSelection().addEventListener('change', 935 this.selectionHandler_.onFileSelectionChanged.bind( 936 this.selectionHandler_)); 937 938 this.initList_(this.grid_); 939 this.initList_(this.table_.list); 940 941 var fileListFocusBound = this.onFileListFocus_.bind(this); 942 var fileListBlurBound = this.onFileListBlur_.bind(this); 943 944 this.table_.list.addEventListener('focus', fileListFocusBound); 945 this.grid_.addEventListener('focus', fileListFocusBound); 946 947 this.table_.list.addEventListener('blur', fileListBlurBound); 948 this.grid_.addEventListener('blur', fileListBlurBound); 949 950 var dragStartBound = this.onDragStart_.bind(this); 951 this.table_.list.addEventListener('dragstart', dragStartBound); 952 this.grid_.addEventListener('dragstart', dragStartBound); 953 954 var dragEndBound = this.onDragEnd_.bind(this); 955 this.table_.list.addEventListener('dragend', dragEndBound); 956 this.grid_.addEventListener('dragend', dragEndBound); 957 // This event is published by DragSelector because drag end event is not 958 // published at the end of drag selection. 959 this.table_.list.addEventListener('dragselectionend', dragEndBound); 960 this.grid_.addEventListener('dragselectionend', dragEndBound); 961 962 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just 963 // attach the directory model. 964 this.initNavigationList_(); 965 966 this.table_.addEventListener('column-resize-end', 967 this.updateStartupPrefs_.bind(this)); 968 969 // Restore preferences. 970 this.directoryModel_.sortFileList( 971 this.viewOptions_.sortField || 'modificationTime', 972 this.viewOptions_.sortDirection || 'desc'); 973 if (this.viewOptions_.columns) { 974 var cm = this.table_.columnModel; 975 for (var i = 0; i < cm.totalSize; i++) { 976 if (this.viewOptions_.columns[i] > 0) 977 cm.setWidth(i, this.viewOptions_.columns[i]); 978 } 979 } 980 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL); 981 982 this.textSearchState_ = {text: '', date: new Date()}; 983 this.closeOnUnmount_ = (this.params_.action == 'auto-open'); 984 985 if (this.closeOnUnmount_) { 986 this.volumeManager_.addEventListener('externally-unmounted', 987 this.onExternallyUnmounted_.bind(this)); 988 } 989 990 // Update metadata to change 'Today' and 'Yesterday' dates. 991 var today = new Date(); 992 today.setHours(0); 993 today.setMinutes(0); 994 today.setSeconds(0); 995 today.setMilliseconds(0); 996 setTimeout(this.dailyUpdateModificationTime_.bind(this), 997 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000); 998 }; 999 1000 /** 1001 * @private 1002 */ 1003 FileManager.prototype.initNavigationList_ = function() { 1004 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree'); 1005 DirectoryTree.decorate(this.directoryTree_, this.directoryModel_); 1006 1007 this.navigationList_ = this.dialogDom_.querySelector('#navigation-list'); 1008 NavigationList.decorate(this.navigationList_, 1009 this.volumeManager_, 1010 this.directoryModel_); 1011 this.navigationList_.fileManager = this; 1012 this.navigationList_.dataModel = new NavigationListModel( 1013 this.volumeManager_, this.folderShortcutsModel_); 1014 }; 1015 1016 /** 1017 * @private 1018 */ 1019 FileManager.prototype.updateMiddleBarVisibility_ = function() { 1020 var entry = this.directoryModel_.getCurrentDirEntry(); 1021 if (!entry) 1022 return; 1023 1024 var driveVolume = this.volumeManager_.getVolumeInfo(entry); 1025 var visible = 1026 DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath) && 1027 driveVolume && !driveVolume.error; 1028 this.dialogDom_. 1029 querySelector('.dialog-middlebar-contents').hidden = !visible; 1030 this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible; 1031 this.onResize_(); 1032 }; 1033 1034 /** 1035 * @private 1036 */ 1037 FileManager.prototype.updateStartupPrefs_ = function() { 1038 var sortStatus = this.directoryModel_.getFileList().sortStatus; 1039 var prefs = { 1040 sortField: sortStatus.field, 1041 sortDirection: sortStatus.direction, 1042 columns: [], 1043 listType: this.listType_ 1044 }; 1045 var cm = this.table_.columnModel; 1046 for (var i = 0; i < cm.totalSize; i++) { 1047 prefs.columns.push(cm.getWidth(i)); 1048 } 1049 // Save the global default. 1050 util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs)); 1051 1052 // Save the window-specific preference. 1053 if (window.appState) { 1054 window.appState.viewOptions = prefs; 1055 util.saveAppState(); 1056 } 1057 }; 1058 1059 FileManager.prototype.refocus = function() { 1060 var targetElement; 1061 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) 1062 targetElement = this.filenameInput_; 1063 else 1064 targetElement = this.currentList_; 1065 1066 // Hack: if the tabIndex is disabled, we can assume a modal dialog is 1067 // shown. Focus to a button on the dialog instead. 1068 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1) 1069 targetElement = document.querySelector('button:not([tabIndex="-1"])'); 1070 1071 if (targetElement) 1072 targetElement.focus(); 1073 }; 1074 1075 /** 1076 * File list focus handler. Used to select the top most element on the list 1077 * if nothing was selected. 1078 * 1079 * @private 1080 */ 1081 FileManager.prototype.onFileListFocus_ = function() { 1082 // Do not select default item if focused using mouse. 1083 if (this.suppressFocus_) 1084 return; 1085 1086 var selection = this.getSelection(); 1087 if (!selection || selection.totalCount != 0) 1088 return; 1089 1090 this.directoryModel_.selectIndex(0); 1091 }; 1092 1093 /** 1094 * File list blur handler. 1095 * 1096 * @private 1097 */ 1098 FileManager.prototype.onFileListBlur_ = function() { 1099 this.suppressFocus_ = false; 1100 }; 1101 1102 /** 1103 * Index of selected item in the typeList of the dialog params. 1104 * 1105 * @return {number} 1-based index of selected type or 0 if no type selected. 1106 * @private 1107 */ 1108 FileManager.prototype.getSelectedFilterIndex_ = function() { 1109 var index = Number(this.fileTypeSelector_.selectedIndex); 1110 if (index < 0) // Nothing selected. 1111 return 0; 1112 if (this.params_.includeAllFiles) // Already 1-based. 1113 return index; 1114 return index + 1; // Convert to 1-based; 1115 }; 1116 1117 FileManager.prototype.setListType = function(type) { 1118 if (type && type == this.listType_) 1119 return; 1120 1121 this.table_.list.startBatchUpdates(); 1122 this.grid_.startBatchUpdates(); 1123 1124 // TODO(dzvorygin): style.display and dataModel setting order shouldn't 1125 // cause any UI bugs. Currently, the only right way is first to set display 1126 // style and only then set dataModel. 1127 1128 if (type == FileManager.ListType.DETAIL) { 1129 this.table_.dataModel = this.directoryModel_.getFileList(); 1130 this.table_.selectionModel = this.directoryModel_.getFileListSelection(); 1131 this.table_.hidden = false; 1132 this.grid_.hidden = true; 1133 this.grid_.selectionModel = this.emptySelectionModel_; 1134 this.grid_.dataModel = this.emptyDataModel_; 1135 this.table_.hidden = false; 1136 /** @type {cr.ui.List} */ 1137 this.currentList_ = this.table_.list; 1138 this.detailViewButton_.setAttribute('checked', ''); 1139 this.thumbnailViewButton_.removeAttribute('checked'); 1140 this.detailViewButton_.setAttribute('disabled', ''); 1141 this.thumbnailViewButton_.removeAttribute('disabled'); 1142 } else if (type == FileManager.ListType.THUMBNAIL) { 1143 this.grid_.dataModel = this.directoryModel_.getFileList(); 1144 this.grid_.selectionModel = this.directoryModel_.getFileListSelection(); 1145 this.grid_.hidden = false; 1146 this.table_.hidden = true; 1147 this.table_.selectionModel = this.emptySelectionModel_; 1148 this.table_.dataModel = this.emptyDataModel_; 1149 this.grid_.hidden = false; 1150 /** @type {cr.ui.List} */ 1151 this.currentList_ = this.grid_; 1152 this.thumbnailViewButton_.setAttribute('checked', ''); 1153 this.detailViewButton_.removeAttribute('checked'); 1154 this.thumbnailViewButton_.setAttribute('disabled', ''); 1155 this.detailViewButton_.removeAttribute('disabled'); 1156 } else { 1157 throw new Error('Unknown list type: ' + type); 1158 } 1159 1160 this.listType_ = type; 1161 this.updateStartupPrefs_(); 1162 this.onResize_(); 1163 1164 this.table_.list.endBatchUpdates(); 1165 this.grid_.endBatchUpdates(); 1166 }; 1167 1168 /** 1169 * Initialize the file list table or grid. 1170 * 1171 * @param {cr.ui.List} list The list. 1172 * @private 1173 */ 1174 FileManager.prototype.initList_ = function(list) { 1175 // Overriding the default role 'list' to 'listbox' for better accessibility 1176 // on ChromeOS. 1177 list.setAttribute('role', 'listbox'); 1178 list.addEventListener('click', this.onDetailClick_.bind(this)); 1179 list.id = 'file-list'; 1180 }; 1181 1182 /** 1183 * @private 1184 */ 1185 FileManager.prototype.onCopyProgress_ = function(event) { 1186 if (event.reason == 'ERROR' && 1187 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR && 1188 event.error.data.toDrive && 1189 event.error.data.code == FileError.QUOTA_EXCEEDED_ERR) { 1190 this.alert.showHtml( 1191 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'), 1192 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE', 1193 decodeURIComponent( 1194 event.error.data.sourceFileUrl.split('/').pop()), 1195 str('GOOGLE_DRIVE_BUY_STORAGE_URL'))); 1196 } 1197 }; 1198 1199 /** 1200 * Handler of file manager operations. Called when an entry has been 1201 * changed. 1202 * This updates directory model to reflect operation result immediately (not 1203 * waiting for directory update event). Also, preloads thumbnails for the 1204 * images of new entries. 1205 * See also FileOperationManager.EventRouter. 1206 * 1207 * @param {Event} event An event for the entry change. 1208 * @private 1209 */ 1210 FileManager.prototype.onEntryChanged_ = function(event) { 1211 var kind = event.kind; 1212 var entry = event.entry; 1213 this.directoryModel_.onEntryChanged(kind, entry); 1214 this.selectionHandler_.onFileSelectionChanged(); 1215 1216 if (kind == util.EntryChangedKind.CREATE && FileType.isImage(entry)) { 1217 // Preload a thumbnail if the new copied entry an image. 1218 var metadata = entry.getMetadata(function(metadata) { 1219 var url = entry.toURL(); 1220 var thumbnailLoader_ = new ThumbnailLoader( 1221 url, 1222 ThumbnailLoader.LoaderType.CANVAS, 1223 metadata, 1224 undefined, // Media type. 1225 FileType.isOnDrive(url) ? 1226 ThumbnailLoader.UseEmbedded.USE_EMBEDDED : 1227 ThumbnailLoader.UseEmbedded.NO_EMBEDDED, 1228 10); // Very low priority. 1229 thumbnailLoader_.loadDetachedImage(function(success) {}); 1230 }); 1231 } 1232 }; 1233 1234 /** 1235 * Fills the file type list or hides it. 1236 * @private 1237 */ 1238 FileManager.prototype.initFileTypeFilter_ = function() { 1239 if (this.params_.includeAllFiles) { 1240 var option = this.document_.createElement('option'); 1241 option.innerText = str('ALL_FILES_FILTER'); 1242 this.fileTypeSelector_.appendChild(option); 1243 option.value = 0; 1244 } 1245 1246 for (var i = 0; i < this.fileTypes_.length; i++) { 1247 var fileType = this.fileTypes_[i]; 1248 var option = this.document_.createElement('option'); 1249 var description = fileType.description; 1250 if (!description) { 1251 // See if all the extensions in the group have the same description. 1252 for (var j = 0; j != fileType.extensions.length; j++) { 1253 var currentDescription = 1254 FileType.getTypeString('.' + fileType.extensions[j]); 1255 if (!description) // Set the first time. 1256 description = currentDescription; 1257 else if (description != currentDescription) { 1258 // No single description, fall through to the extension list. 1259 description = null; 1260 break; 1261 } 1262 } 1263 1264 if (!description) 1265 // Convert ['jpg', 'png'] to '*.jpg, *.png'. 1266 description = fileType.extensions.map(function(s) { 1267 return '*.' + s; 1268 }).join(', '); 1269 } 1270 option.innerText = description; 1271 1272 option.value = i + 1; 1273 1274 if (fileType.selected) 1275 option.selected = true; 1276 1277 this.fileTypeSelector_.appendChild(option); 1278 } 1279 1280 var options = this.fileTypeSelector_.querySelectorAll('option'); 1281 if (options.length >= 2) { 1282 // There is in fact no choice, show the selector. 1283 this.fileTypeSelector_.hidden = false; 1284 1285 this.fileTypeSelector_.addEventListener('change', 1286 this.updateFileTypeFilter_.bind(this)); 1287 } 1288 }; 1289 1290 /** 1291 * Filters file according to the selected file type. 1292 * @private 1293 */ 1294 FileManager.prototype.updateFileTypeFilter_ = function() { 1295 this.fileFilter_.removeFilter('fileType'); 1296 var selectedIndex = this.getSelectedFilterIndex_(); 1297 if (selectedIndex > 0) { // Specific filter selected. 1298 var regexp = new RegExp('.*(' + 1299 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i'); 1300 var filter = function(entry) { 1301 return entry.isDirectory || regexp.test(entry.name); 1302 }; 1303 this.fileFilter_.addFilter('fileType', filter); 1304 } 1305 }; 1306 1307 /** 1308 * Resize details and thumb views to fit the new window size. 1309 * @private 1310 */ 1311 FileManager.prototype.onResize_ = function() { 1312 if (this.listType_ == FileManager.ListType.THUMBNAIL) 1313 this.grid_.relayout(); 1314 else 1315 this.table_.relayout(); 1316 1317 // May not be available during initialization. 1318 if (this.directoryTree_) 1319 this.directoryTree_.relayout(); 1320 1321 // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before 1322 // file system is available. 1323 if (this.navigationList_) 1324 this.navigationList_.redraw(); 1325 1326 this.ui_.searchBox.updateSizeRelatedStyle(); 1327 1328 this.previewPanel_.breadcrumbs.truncate(); 1329 }; 1330 1331 /** 1332 * Handles local metadata changes in the currect directory. 1333 * @param {Event} event Change event. 1334 * @private 1335 */ 1336 FileManager.prototype.onWatcherMetadataChanged_ = function(event) { 1337 this.updateMetadataInUI_( 1338 event.metadataType, event.entries, event.properties); 1339 }; 1340 1341 /** 1342 * Resize details and thumb views to fit the new window size. 1343 * @private 1344 */ 1345 FileManager.prototype.onPreviewPanelVisibilityChange_ = function() { 1346 // This method may be called on initialization. Some object may not be 1347 // initialized. 1348 1349 var panelHeight = this.previewPanel_.visible ? 1350 this.previewPanel_.height : 0; 1351 if (this.grid_) 1352 this.grid_.setBottomMarginForPanel(panelHeight); 1353 if (this.table_) 1354 this.table_.setBottomMarginForPanel(panelHeight); 1355 1356 if (this.directoryTree_) { 1357 this.directoryTree_.setBottomMarginForPanel(panelHeight); 1358 this.ensureDirectoryTreeItemNotBehindPreviewPanel_(); 1359 } 1360 }; 1361 1362 /** 1363 * Invoked when the drag is started on the list or the grid. 1364 * @private 1365 */ 1366 FileManager.prototype.onDragStart_ = function() { 1367 // On open file dialog, the preview panel is always shown. 1368 if (DialogType.isOpenDialog(this.dialogType)) 1369 return; 1370 this.previewPanel_.visibilityType = 1371 PreviewPanel.VisibilityType.ALWAYS_HIDDEN; 1372 }; 1373 1374 /** 1375 * Invoked when the drag is ended on the list or the grid. 1376 * @private 1377 */ 1378 FileManager.prototype.onDragEnd_ = function() { 1379 // On open file dialog, the preview panel is always shown. 1380 if (DialogType.isOpenDialog(this.dialogType)) 1381 return; 1382 this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO; 1383 }; 1384 1385 /** 1386 * Restores current directory and may be a selected item after page load (or 1387 * reload) or popping a state (after click on back/forward). defaultPath 1388 * primarily is used with save/open dialogs. 1389 * Default path may also contain a file name. Freshly opened file manager 1390 * window has neither. 1391 * 1392 * @private 1393 */ 1394 FileManager.prototype.setupCurrentDirectory_ = function() { 1395 var tracker = this.directoryModel_.createDirectoryChangeTracker(); 1396 var queue = new AsyncUtil.Queue(); 1397 1398 // Wait until the volume manager is initialized. 1399 queue.run(function(callback) { 1400 tracker.start(); 1401 this.volumeManager_.ensureInitialized(callback); 1402 }.bind(this)); 1403 1404 // Resolve the default path. 1405 var defaultFullPath; 1406 var candidateFullPath; 1407 var candidateEntry; 1408 queue.run(function(callback) { 1409 // Cancel this sequence if the current directory has already changed. 1410 if (tracker.hasChanged) { 1411 callback(); 1412 return; 1413 } 1414 1415 // Resolve the absolute path in case only the file name or an empty string 1416 // is passed. 1417 if (!this.defaultPath) { 1418 defaultFullPath = PathUtil.DEFAULT_MOUNT_POINT; 1419 } else if (this.defaultPath.indexOf('/') === -1) { 1420 // Path is a file name. 1421 defaultFullPath = PathUtil.DEFAULT_MOUNT_POINT + '/' + this.defaultPath; 1422 } else { 1423 defaultFullPath = this.defaultPath; 1424 } 1425 1426 // If Drive is disabled but the path points to Drive's entry, fallback to 1427 // DEFAULT_MOUNT_POINT. 1428 if (PathUtil.isDriveBasedPath(defaultFullPath) && 1429 !this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) { 1430 candidateFullPath = PathUtil.DEFAULT_MOUNT_POINT + '/' + 1431 PathUtil.basename(defaultFullPath); 1432 } else { 1433 candidateFullPath = defaultFullPath; 1434 } 1435 1436 // If the path points a fake entry, use the entry directly. 1437 var fakeEntries = DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES; 1438 for (var i = 0; i < fakeEntries.length; i++) { 1439 if (candidateFullPath === fakeEntries[i].fullPath) { 1440 candidateEntry = fakeEntries[i]; 1441 callback(); 1442 return; 1443 } 1444 } 1445 1446 // Convert the path to the directory entry and an optional selection 1447 // entry. 1448 // TODO(hirono): There may be a race here. The path on Drive, may not 1449 // be available yet. 1450 this.volumeManager_.resolveAbsolutePath(candidateFullPath, 1451 function(inEntry) { 1452 candidateEntry = inEntry; 1453 callback(); 1454 }, function() { 1455 callback(); 1456 }); 1457 }.bind(this)); 1458 1459 // Check the obtained entry. 1460 var nextCurrentDirEntry; 1461 var selectionEntry = null; 1462 var suggestedName = null; 1463 var error = null; 1464 queue.run(function(callback) { 1465 // Cancel this sequence if the current directory has already changed. 1466 if (tracker.hasChanged) { 1467 callback(); 1468 return; 1469 } 1470 1471 if (candidateEntry) { 1472 // The entry is directory. Use it. 1473 if (candidateEntry.isDirectory) { 1474 nextCurrentDirEntry = candidateEntry; 1475 callback(); 1476 return; 1477 } 1478 // The entry exists, but it is not a directory. Therefore use a 1479 // parent. 1480 candidateEntry.getParent(function(parentEntry) { 1481 nextCurrentDirEntry = parentEntry; 1482 selectionEntry = candidateEntry; 1483 callback(); 1484 }, function() { 1485 error = new Error('Unable to resolve parent for: ' + 1486 candidateEntry.fullPath); 1487 callback(); 1488 }); 1489 return; 1490 } 1491 1492 // If the entry doesn't exist, most probably because the path contains a 1493 // suggested name. Therefore try to open its parent. However, the parent 1494 // may also not exist. In such situation, fallback. 1495 var pathNodes = candidateFullPath.split('/'); 1496 var baseName = pathNodes.pop(); 1497 var parentPath = pathNodes.join('/'); 1498 this.volumeManager_.resolveAbsolutePath( 1499 parentPath, 1500 function(parentEntry) { 1501 nextCurrentDirEntry = parentEntry; 1502 suggestedName = baseName; 1503 callback(); 1504 }, 1505 callback); // In case of an error, continue. 1506 }.bind(this)); 1507 1508 // If the directory is not set at this stage, fallback to the default 1509 // mount point. 1510 queue.run(function(callback) { 1511 // Cancel this sequence if the current directory has already changed, 1512 // or the next current directory is already set. 1513 if (tracker.hasChanged || nextCurrentDirEntry) { 1514 callback(); 1515 return; 1516 } 1517 this.volumeManager_.resolveAbsolutePath( 1518 PathUtil.DEFAULT_MOUNT_POINT, 1519 function(fallbackEntry) { 1520 nextCurrentDirEntry = fallbackEntry; 1521 callback(); 1522 }, 1523 function() { 1524 // Fallback directory not available? Throw an error. 1525 error = new Error('Unable to resolve the fallback directory: ' + 1526 PathUtil.DEFAULT_MOUNT_POINT); 1527 callback(); 1528 }); 1529 }.bind(this)); 1530 1531 queue.run(function(callback) { 1532 // Check error. 1533 if (error) { 1534 callback(); 1535 throw error; 1536 } 1537 // Check directory change. 1538 tracker.stop(); 1539 if (tracker.hasChanged) { 1540 callback(); 1541 return; 1542 } 1543 // Finish setup current directory. 1544 this.finishSetupCurrentDirectory_( 1545 nextCurrentDirEntry, selectionEntry, suggestedName); 1546 callback(); 1547 }.bind(this)); 1548 }; 1549 1550 /** 1551 * @param {DirectoryEntry} directoryEntry Directory to be opened. 1552 * @param {Entry=} opt_selectionEntry Entry to be selected. 1553 * @param {string=} opt_suggestedName Suggested name for a non-existing\ 1554 * selection. 1555 * @private 1556 */ 1557 FileManager.prototype.finishSetupCurrentDirectory_ = function( 1558 directoryEntry, opt_selectionEntry, opt_suggestedName) { 1559 // Open the directory, and select the selection (if passed). 1560 if (util.isFakeEntry(directoryEntry)) { 1561 this.directoryModel_.specialSearch(directoryEntry.fullPath, ''); 1562 } else { 1563 this.directoryModel_.changeDirectoryEntry(directoryEntry, function() { 1564 if (opt_selectionEntry) 1565 this.directoryModel_.selectEntry(opt_selectionEntry); 1566 }.bind(this)); 1567 } 1568 1569 if (this.dialogType == DialogType.FULL_PAGE) { 1570 // In the FULL_PAGE mode if the restored path points to a file we might 1571 // have to invoke a task after selecting it. 1572 if (this.params_.action == 'select') 1573 return; 1574 1575 var task = null; 1576 if (opt_suggestedName) { 1577 // Non-existent file or a directory. 1578 if (this.params_.gallery) { 1579 // Reloading while the Gallery is open with empty or multiple 1580 // selection. Open the Gallery when the directory is scanned. 1581 task = function() { 1582 new FileTasks(this, this.params_).openGallery([]); 1583 }.bind(this); 1584 } 1585 } else if (opt_selectionEntry) { 1586 // There is a file to be selected. It means, that we are recovering 1587 // the Files app. 1588 // We call the appropriate methods of FileTasks directly as we do 1589 // not need any of the preparations that |execute| method does. 1590 // TODO(mtomasz): Change Entry.fullPath to Entry. 1591 var mediaType = FileType.getMediaType(opt_selectionEntry.fullPath); 1592 if (mediaType == 'image' || mediaType == 'video') { 1593 task = function() { 1594 // TODO(mtomasz): Replace the url with an entry. 1595 new FileTasks(this, this.params_).openGallery([opt_selectionEntry]); 1596 }.bind(this); 1597 } else if (mediaType == 'archive') { 1598 task = function() { 1599 new FileTasks(this, this.params_).mountArchives( 1600 [opt_selectionEntry]); 1601 }.bind(this); 1602 } 1603 } 1604 1605 // If there is a task to be run, run it after the scan is completed. 1606 if (task) { 1607 var listener = function() { 1608 this.directoryModel_.removeEventListener( 1609 'scan-completed', listener); 1610 task(); 1611 }.bind(this); 1612 this.directoryModel_.addEventListener('scan-completed', listener); 1613 } 1614 } else if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) { 1615 this.filenameInput_.value = opt_suggestedName || ''; 1616 this.selectDefaultPathInFilenameInput_(); 1617 } 1618 }; 1619 1620 /** 1621 * Unmounts device. 1622 * @param {string} path Path to a volume to unmount. 1623 */ 1624 FileManager.prototype.unmountVolume = function(path) { 1625 var onError = function(error) { 1626 this.alert.showHtml('', str('UNMOUNT_FAILED')); 1627 }; 1628 this.volumeManager_.unmount(path, function() {}, onError.bind(this)); 1629 }; 1630 1631 /** 1632 * @private 1633 */ 1634 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() { 1635 var entries = this.directoryModel_.getFileList().slice(); 1636 var directoryEntry = this.directoryModel_.getCurrentDirEntry(); 1637 if (!directoryEntry) 1638 return; 1639 // We don't pass callback here. When new metadata arrives, we have an 1640 // observer registered to update the UI. 1641 1642 // TODO(dgozman): refresh content metadata only when modificationTime 1643 // changed. 1644 var isFakeEntry = util.isFakeEntry(directoryEntry); 1645 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries); 1646 if (!isFakeEntry) 1647 this.metadataCache_.clearRecursively(directoryEntry, '*'); 1648 this.metadataCache_.get(getEntries, 'filesystem', null); 1649 1650 if (this.isOnDrive()) 1651 this.metadataCache_.get(getEntries, 'drive', null); 1652 1653 var visibleItems = this.currentList_.items; 1654 var visibleEntries = []; 1655 for (var i = 0; i < visibleItems.length; i++) { 1656 var index = this.currentList_.getIndexOfListItem(visibleItems[i]); 1657 var entry = this.directoryModel_.getFileList().item(index); 1658 // The following check is a workaround for the bug in list: sometimes item 1659 // does not have listIndex, and therefore is not found in the list. 1660 if (entry) visibleEntries.push(entry); 1661 } 1662 this.metadataCache_.get(visibleEntries, 'thumbnail', null); 1663 }; 1664 1665 /** 1666 * @private 1667 */ 1668 FileManager.prototype.dailyUpdateModificationTime_ = function() { 1669 var fileList = this.directoryModel_.getFileList(); 1670 var entries = []; 1671 for (var i = 0; i < fileList.length; i++) { 1672 entries.push(fileList.item(i)); 1673 } 1674 this.metadataCache_.get( 1675 entries, 1676 'filesystem', 1677 this.updateMetadataInUI_.bind(this, 'filesystem', entries)); 1678 1679 setTimeout(this.dailyUpdateModificationTime_.bind(this), 1680 MILLISECONDS_IN_DAY); 1681 }; 1682 1683 /** 1684 * @param {string} type Type of metadata changed. 1685 * @param {Array.<Entry>} entries Array of entries. 1686 * @param {Object.<string, Object>} props Map from entry URLs to metadata 1687 * props. 1688 * @private 1689 */ 1690 FileManager.prototype.updateMetadataInUI_ = function( 1691 type, entries, properties) { 1692 if (this.listType_ == FileManager.ListType.DETAIL) 1693 this.table_.updateListItemsMetadata(type, properties); 1694 else 1695 this.grid_.updateListItemsMetadata(type, properties); 1696 // TODO: update bottom panel thumbnails. 1697 }; 1698 1699 /** 1700 * Restore the item which is being renamed while refreshing the file list. Do 1701 * nothing if no item is being renamed or such an item disappeared. 1702 * 1703 * While refreshing file list it gets repopulated with new file entries. 1704 * There is not a big difference whether DOM items stay the same or not. 1705 * Except for the item that the user is renaming. 1706 * 1707 * @private 1708 */ 1709 FileManager.prototype.restoreItemBeingRenamed_ = function() { 1710 if (!this.isRenamingInProgress()) 1711 return; 1712 1713 var dm = this.directoryModel_; 1714 var leadIndex = dm.getFileListSelection().leadIndex; 1715 if (leadIndex < 0) 1716 return; 1717 1718 var leadEntry = dm.getFileList().item(leadIndex); 1719 if (this.renameInput_.currentEntry.fullPath != leadEntry.fullPath) 1720 return; 1721 1722 var leadListItem = this.findListItemForNode_(this.renameInput_); 1723 if (this.currentList_ == this.table_.list) { 1724 this.table_.updateFileMetadata(leadListItem, leadEntry); 1725 } 1726 this.currentList_.restoreLeadItem(leadListItem); 1727 }; 1728 1729 /** 1730 * @return {boolean} True if the current directory content is from Google 1731 * Drive. 1732 */ 1733 FileManager.prototype.isOnDrive = function() { 1734 var rootType = this.directoryModel_.getCurrentRootType(); 1735 return rootType === RootType.DRIVE || 1736 rootType === RootType.DRIVE_SHARED_WITH_ME || 1737 rootType === RootType.DRIVE_RECENT || 1738 rootType === RootType.DRIVE_OFFLINE; 1739 }; 1740 1741 /** 1742 * Overrides default handling for clicks on hyperlinks. 1743 * In a packaged apps links with targer='_blank' open in a new tab by 1744 * default, other links do not open at all. 1745 * 1746 * @param {Event} event Click event. 1747 * @private 1748 */ 1749 FileManager.prototype.onExternalLinkClick_ = function(event) { 1750 if (event.target.tagName != 'A' || !event.target.href) 1751 return; 1752 1753 if (this.dialogType != DialogType.FULL_PAGE) 1754 this.onCancel_(); 1755 }; 1756 1757 /** 1758 * Task combobox handler. 1759 * 1760 * @param {Object} event Event containing task which was clicked. 1761 * @private 1762 */ 1763 FileManager.prototype.onTaskItemClicked_ = function(event) { 1764 var selection = this.getSelection(); 1765 if (!selection.tasks) return; 1766 1767 if (event.item.task) { 1768 // Task field doesn't exist on change-default dropdown item. 1769 selection.tasks.execute(event.item.task.taskId); 1770 } else { 1771 var extensions = []; 1772 1773 for (var i = 0; i < selection.entries.length; i++) { 1774 var match = /\.(\w+)$/g.exec(selection.entries[i].toURL()); 1775 if (match) { 1776 var ext = match[1].toUpperCase(); 1777 if (extensions.indexOf(ext) == -1) { 1778 extensions.push(ext); 1779 } 1780 } 1781 } 1782 1783 var format = ''; 1784 1785 if (extensions.length == 1) { 1786 format = extensions[0]; 1787 } 1788 1789 // Change default was clicked. We should open "change default" dialog. 1790 selection.tasks.showTaskPicker(this.defaultTaskPicker, 1791 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'), 1792 strf('CHANGE_DEFAULT_CAPTION', format), 1793 this.onDefaultTaskDone_.bind(this)); 1794 } 1795 }; 1796 1797 /** 1798 * Sets the given task as default, when this task is applicable. 1799 * 1800 * @param {Object} task Task to set as default. 1801 * @private 1802 */ 1803 FileManager.prototype.onDefaultTaskDone_ = function(task) { 1804 // TODO(dgozman): move this method closer to tasks. 1805 var selection = this.getSelection(); 1806 chrome.fileBrowserPrivate.setDefaultTask( 1807 task.taskId, 1808 util.entriesToURLs(selection.entries), 1809 selection.mimeTypes); 1810 selection.tasks = new FileTasks(this); 1811 selection.tasks.init(selection.entries, selection.mimeTypes); 1812 selection.tasks.display(this.taskItems_); 1813 this.refreshCurrentDirectoryMetadata_(); 1814 this.selectionHandler_.onFileSelectionChanged(); 1815 }; 1816 1817 /** 1818 * @private 1819 */ 1820 FileManager.prototype.onPreferencesChanged_ = function() { 1821 var self = this; 1822 this.getPreferences_(function(prefs) { 1823 self.initDateTimeFormatters_(); 1824 self.refreshCurrentDirectoryMetadata_(); 1825 1826 if (prefs.cellularDisabled) 1827 self.syncButton.setAttribute('checked', ''); 1828 else 1829 self.syncButton.removeAttribute('checked'); 1830 1831 if (self.hostedButton.hasAttribute('checked') != 1832 prefs.hostedFilesDisabled && self.isOnDrive()) { 1833 self.directoryModel_.rescan(); 1834 } 1835 1836 if (!prefs.hostedFilesDisabled) 1837 self.hostedButton.setAttribute('checked', ''); 1838 else 1839 self.hostedButton.removeAttribute('checked'); 1840 }, 1841 true /* refresh */); 1842 }; 1843 1844 FileManager.prototype.onDriveConnectionChanged_ = function() { 1845 var connection = this.volumeManager_.getDriveConnectionState(); 1846 if (this.commandHandler) 1847 this.commandHandler.updateAvailability(); 1848 if (this.dialogContainer_) 1849 this.dialogContainer_.setAttribute('connection', connection.type); 1850 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR); 1851 this.suggestAppsDialog.onDriveConnectionChanged(connection.type); 1852 }; 1853 1854 /** 1855 * Get the metered status of Drive connection. 1856 * 1857 * @return {boolean} Returns true if drive should limit the traffic because 1858 * the connection is metered and the 'disable-sync-on-metered' setting is 1859 * enabled. Otherwise, returns false. 1860 */ 1861 FileManager.prototype.isDriveOnMeteredConnection = function() { 1862 var connection = this.volumeManager_.getDriveConnectionState(); 1863 return connection.type == util.DriveConnectionType.METERED; 1864 }; 1865 1866 /** 1867 * Get the online/offline status of drive. 1868 * 1869 * @return {boolean} Returns true if the connection is offline. Otherwise, 1870 * returns false. 1871 */ 1872 FileManager.prototype.isDriveOffline = function() { 1873 var connection = this.volumeManager_.getDriveConnectionState(); 1874 return connection.type == util.DriveConnectionType.OFFLINE; 1875 }; 1876 1877 FileManager.prototype.isOnReadonlyDirectory = function() { 1878 return this.directoryModel_.isReadOnly(); 1879 }; 1880 1881 /** 1882 * @param {Event} Unmount event. 1883 * @private 1884 */ 1885 FileManager.prototype.onExternallyUnmounted_ = function(event) { 1886 if (event.mountPath == this.directoryModel_.getCurrentRootPath()) { 1887 if (this.closeOnUnmount_) { 1888 // If the file manager opened automatically when a usb drive inserted, 1889 // user have never changed current volume (that implies the current 1890 // directory is still on the device) then close this window. 1891 window.close(); 1892 } 1893 } 1894 }; 1895 1896 /** 1897 * Show a modal-like file viewer/editor on top of the File Manager UI. 1898 * 1899 * @param {HTMLElement} popup Popup element. 1900 * @param {function()} closeCallback Function to call after the popup is 1901 * closed. 1902 */ 1903 FileManager.prototype.openFilePopup = function(popup, closeCallback) { 1904 this.closeFilePopup(); 1905 this.filePopup_ = popup; 1906 this.filePopupCloseCallback_ = closeCallback; 1907 this.dialogDom_.insertBefore( 1908 this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area')); 1909 this.filePopup_.focus(); 1910 this.document_.body.setAttribute('overlay-visible', ''); 1911 this.document_.querySelector('#iframe-drag-area').hidden = false; 1912 }; 1913 1914 /** 1915 * Closes the modal-like file viewer/editor popup. 1916 */ 1917 FileManager.prototype.closeFilePopup = function() { 1918 if (this.filePopup_) { 1919 this.document_.body.removeAttribute('overlay-visible'); 1920 this.document_.querySelector('#iframe-drag-area').hidden = true; 1921 // The window resize would not be processed properly while the relevant 1922 // divs had 'display:none', force resize after the layout fired. 1923 setTimeout(this.onResize_.bind(this), 0); 1924 if (this.filePopup_.contentWindow && 1925 this.filePopup_.contentWindow.unload) { 1926 this.filePopup_.contentWindow.unload(); 1927 } 1928 1929 if (this.filePopupCloseCallback_) { 1930 this.filePopupCloseCallback_(); 1931 this.filePopupCloseCallback_ = null; 1932 } 1933 1934 // These operations have to be in the end, otherwise v8 crashes on an 1935 // assert. See: crbug.com/224174. 1936 this.dialogDom_.removeChild(this.filePopup_); 1937 this.filePopup_ = null; 1938 } 1939 }; 1940 1941 /** 1942 * Updates visibility of the draggable app region in the modal-like file 1943 * viewer/editor. 1944 * 1945 * @param {boolean} visible True for visible, false otherwise. 1946 */ 1947 FileManager.prototype.onFilePopupAppRegionChanged = function(visible) { 1948 if (!this.filePopup_) 1949 return; 1950 1951 this.document_.querySelector('#iframe-drag-area').hidden = !visible; 1952 }; 1953 1954 /** 1955 * @return {Array.<Entry>} List of all entries in the current directory. 1956 */ 1957 FileManager.prototype.getAllEntriesInCurrentDirectory = function() { 1958 return this.directoryModel_.getFileList().slice(); 1959 }; 1960 1961 FileManager.prototype.isRenamingInProgress = function() { 1962 return !!this.renameInput_.currentEntry; 1963 }; 1964 1965 /** 1966 * @private 1967 */ 1968 FileManager.prototype.focusCurrentList_ = function() { 1969 if (this.listType_ == FileManager.ListType.DETAIL) 1970 this.table_.focus(); 1971 else // this.listType_ == FileManager.ListType.THUMBNAIL) 1972 this.grid_.focus(); 1973 }; 1974 1975 /** 1976 * Return full path of the current directory or null. 1977 * @return {?string} The full path of the current directory. 1978 */ 1979 FileManager.prototype.getCurrentDirectory = function() { 1980 return this.directoryModel_ && this.directoryModel_.getCurrentDirPath(); 1981 }; 1982 1983 /** 1984 * Return URL of the current directory or null. 1985 * @return {string} URL representing the current directory. 1986 */ 1987 FileManager.prototype.getCurrentDirectoryURL = function() { 1988 return this.directoryModel_ && 1989 this.directoryModel_.getCurrentDirectoryURL(); 1990 }; 1991 1992 /** 1993 * Return DirectoryEntry of the current directory or null. 1994 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns 1995 * null if the directory model is not ready or the current directory is 1996 * not set. 1997 */ 1998 FileManager.prototype.getCurrentDirectoryEntry = function() { 1999 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry(); 2000 }; 2001 2002 /** 2003 * Deletes the selected file and directories recursively. 2004 */ 2005 FileManager.prototype.deleteSelection = function() { 2006 // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364 2007 var entries = this.getSelection().entries; 2008 var message = entries.length == 1 ? 2009 strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) : 2010 strf('GALLERY_CONFIRM_DELETE_SOME', entries.length); 2011 this.confirm.show(message, function() { 2012 this.fileOperationManager_.deleteEntries(entries); 2013 }.bind(this)); 2014 }; 2015 2016 /** 2017 * Shows the share dialog for the selected file or directory. 2018 */ 2019 FileManager.prototype.shareSelection = function() { 2020 var entries = this.getSelection().entries; 2021 if (entries.length != 1) { 2022 console.warn('Unable to share multiple items at once.'); 2023 return; 2024 } 2025 // Add the overlapped class to prevent the applicaiton window from 2026 // captureing mouse events. 2027 this.shareDialog_.show(entries[0], function(result) { 2028 if (result == ShareDialog.Result.NETWORK_ERROR) 2029 this.error.show(str('SHARE_ERROR')); 2030 }.bind(this)); 2031 }; 2032 2033 /** 2034 * Creates a folder shortcut. 2035 * @param {string} path A shortcut which refers to |path| to be created. 2036 */ 2037 FileManager.prototype.createFolderShortcut = function(path) { 2038 // Duplicate entry. 2039 if (this.folderShortcutExists(path)) 2040 return; 2041 2042 this.folderShortcutsModel_.add(path); 2043 }; 2044 2045 /** 2046 * Checkes if the shortcut which refers to the given folder exists or not. 2047 * @param {string} path Path of the folder to be checked. 2048 */ 2049 FileManager.prototype.folderShortcutExists = function(path) { 2050 return this.folderShortcutsModel_.exists(path); 2051 }; 2052 2053 /** 2054 * Removes the folder shortcut. 2055 * @param {string} path The shortcut which refers to |path| is to be removed. 2056 */ 2057 FileManager.prototype.removeFolderShortcut = function(path) { 2058 this.folderShortcutsModel_.remove(path); 2059 }; 2060 2061 /** 2062 * Blinks the selection. Used to give feedback when copying or cutting the 2063 * selection. 2064 */ 2065 FileManager.prototype.blinkSelection = function() { 2066 var selection = this.getSelection(); 2067 if (!selection || selection.totalCount == 0) 2068 return; 2069 2070 for (var i = 0; i < selection.entries.length; i++) { 2071 var selectedIndex = selection.indexes[i]; 2072 var listItem = this.currentList_.getListItemByIndex(selectedIndex); 2073 if (listItem) 2074 this.blinkListItem_(listItem); 2075 } 2076 }; 2077 2078 /** 2079 * @param {Element} listItem List item element. 2080 * @private 2081 */ 2082 FileManager.prototype.blinkListItem_ = function(listItem) { 2083 listItem.classList.add('blink'); 2084 setTimeout(function() { 2085 listItem.classList.remove('blink'); 2086 }, 100); 2087 }; 2088 2089 /** 2090 * @private 2091 */ 2092 FileManager.prototype.selectDefaultPathInFilenameInput_ = function() { 2093 var input = this.filenameInput_; 2094 input.focus(); 2095 var selectionEnd = input.value.lastIndexOf('.'); 2096 if (selectionEnd == -1) { 2097 input.select(); 2098 } else { 2099 input.selectionStart = 0; 2100 input.selectionEnd = selectionEnd; 2101 } 2102 // Clear, so we never do this again. 2103 this.defaultPath = ''; 2104 }; 2105 2106 /** 2107 * Handles mouse click or tap. 2108 * 2109 * @param {Event} event The click event. 2110 * @private 2111 */ 2112 FileManager.prototype.onDetailClick_ = function(event) { 2113 if (this.isRenamingInProgress()) { 2114 // Don't pay attention to clicks during a rename. 2115 return; 2116 } 2117 2118 var listItem = this.findListItemForEvent_(event); 2119 var selection = this.getSelection(); 2120 if (!listItem || !listItem.selected || selection.totalCount != 1) { 2121 return; 2122 } 2123 2124 // React on double click, but only if both clicks hit the same item. 2125 // TODO(mtomasz): Simplify it, and use a double click handler if possible. 2126 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined; 2127 this.lastClickedItem_ = listItem; 2128 2129 if (event.detail != clickNumber) 2130 return; 2131 2132 var entry = selection.entries[0]; 2133 if (entry.isDirectory) { 2134 this.onDirectoryAction_(entry); 2135 } else { 2136 this.dispatchSelectionAction_(); 2137 } 2138 }; 2139 2140 /** 2141 * @private 2142 */ 2143 FileManager.prototype.dispatchSelectionAction_ = function() { 2144 if (this.dialogType == DialogType.FULL_PAGE) { 2145 var selection = this.getSelection(); 2146 var tasks = selection.tasks; 2147 var urls = selection.urls; 2148 var mimeTypes = selection.mimeTypes; 2149 if (tasks) 2150 tasks.executeDefault(); 2151 return true; 2152 } 2153 if (!this.okButton_.disabled) { 2154 this.onOk_(); 2155 return true; 2156 } 2157 return false; 2158 }; 2159 2160 /** 2161 * Opens the suggest file dialog. 2162 * 2163 * @param {Entry} entry Entry of the file. 2164 * @param {function()} onSuccess Success callback. 2165 * @param {function()} onCancelled User-cancelled callback. 2166 * @param {function()} onFailure Failure callback. 2167 * @private 2168 */ 2169 FileManager.prototype.openSuggestAppsDialog = 2170 function(entry, onSuccess, onCancelled, onFailure) { 2171 if (!url) { 2172 onFailure(); 2173 return; 2174 } 2175 2176 this.metadataCache_.get([entry], 'drive', function(props) { 2177 if (!props || !props[0] || !props[0].contentMimeType) { 2178 onFailure(); 2179 return; 2180 } 2181 2182 var basename = entry.name; 2183 var splitted = PathUtil.splitExtension(basename); 2184 var filename = splitted[0]; 2185 var extension = splitted[1]; 2186 var mime = props[0].contentMimeType; 2187 2188 // Returns with failure if the file has neither extension nor mime. 2189 if (!extension || !mime) { 2190 onFailure(); 2191 return; 2192 } 2193 2194 var onDialogClosed = function(result) { 2195 switch (result) { 2196 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL: 2197 onSuccess(); 2198 break; 2199 case SuggestAppsDialog.Result.FAILED: 2200 onFailure(); 2201 break; 2202 default: 2203 onCancelled(); 2204 } 2205 }; 2206 2207 if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) { 2208 this.suggestAppsDialog.showByFilename(filename, onDialogClosed); 2209 } else { 2210 this.suggestAppsDialog.showByExtensionAndMime( 2211 extension, mime, onDialogClosed); 2212 } 2213 }.bind(this)); 2214 }; 2215 2216 /** 2217 * Called when a dialog is shown or hidden. 2218 * @param {boolean} flag True if a dialog is shown, false if hidden. */ 2219 FileManager.prototype.onDialogShownOrHidden = function(show) { 2220 // Set/unset a flag to disable dragging on the title area. 2221 this.dialogContainer_.classList.toggle('disable-header-drag', show); 2222 }; 2223 2224 /** 2225 * Executes directory action (i.e. changes directory). 2226 * 2227 * @param {DirectoryEntry} entry Directory entry to which directory should be 2228 * changed. 2229 * @private 2230 */ 2231 FileManager.prototype.onDirectoryAction_ = function(entry) { 2232 return this.directoryModel_.changeDirectory(entry.fullPath); 2233 }; 2234 2235 /** 2236 * Update the window title. 2237 * @private 2238 */ 2239 FileManager.prototype.updateTitle_ = function() { 2240 if (this.dialogType != DialogType.FULL_PAGE) 2241 return; 2242 2243 var path = this.getCurrentDirectory(); 2244 var rootPath = PathUtil.getRootPath(path); 2245 this.document_.title = PathUtil.getRootLabel(rootPath) + 2246 path.substring(rootPath.length); 2247 }; 2248 2249 /** 2250 * Update the gear menu. 2251 * @private 2252 */ 2253 FileManager.prototype.updateGearMenu_ = function() { 2254 var hideItemsForDrive = !this.isOnDrive(); 2255 this.syncButton.hidden = hideItemsForDrive; 2256 this.hostedButton.hidden = hideItemsForDrive; 2257 this.document_.getElementById('drive-separator').hidden = 2258 hideItemsForDrive; 2259 2260 // If volume has changed, then fetch remaining space data. 2261 if (this.previousRootUrl_ != this.directoryModel_.getCurrentMountPointUrl()) 2262 this.refreshRemainingSpace_(true); // Show loading caption. 2263 2264 this.previousRootUrl_ = this.directoryModel_.getCurrentMountPointUrl(); 2265 }; 2266 2267 /** 2268 * Refreshes space info of the current volume. 2269 * @param {boolean} showLoadingCaption Whether show loading caption or not. 2270 * @private 2271 */ 2272 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) { 2273 var volumeSpaceInfoLabel = 2274 this.dialogDom_.querySelector('#volume-space-info-label'); 2275 var volumeSpaceInnerBar = 2276 this.dialogDom_.querySelector('#volume-space-info-bar'); 2277 var volumeSpaceOuterBar = 2278 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode; 2279 2280 volumeSpaceInnerBar.setAttribute('pending', ''); 2281 2282 if (showLoadingCaption) { 2283 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO'); 2284 volumeSpaceInnerBar.style.width = '100%'; 2285 } 2286 2287 var currentMountPointUrl = this.directoryModel_.getCurrentMountPointUrl(); 2288 chrome.fileBrowserPrivate.getSizeStats( 2289 currentMountPointUrl, function(result) { 2290 if (this.directoryModel_.getCurrentMountPointUrl() != 2291 currentMountPointUrl) 2292 return; 2293 updateSpaceInfo(result, 2294 volumeSpaceInnerBar, 2295 volumeSpaceInfoLabel, 2296 volumeSpaceOuterBar); 2297 }.bind(this)); 2298 }; 2299 2300 /** 2301 * Update the UI when the current directory changes. 2302 * 2303 * @param {Event} event The directory-changed event. 2304 * @private 2305 */ 2306 FileManager.prototype.onDirectoryChanged_ = function(event) { 2307 this.selectionHandler_.onFileSelectionChanged(); 2308 this.ui_.searchBox.clear(); 2309 util.updateAppState(this.getCurrentDirectory()); 2310 2311 // If the current directory is moved from the device's volume, do not 2312 // automatically close the window on device removal. 2313 if (event.previousDirEntry && 2314 PathUtil.getRootPath(event.previousDirEntry.fullPath) != 2315 PathUtil.getRootPath(event.newDirEntry.fullPath)) 2316 this.closeOnUnmount_ = false; 2317 2318 if (this.commandHandler) 2319 this.commandHandler.updateAvailability(); 2320 this.updateUnformattedVolumeStatus_(); 2321 this.updateTitle_(); 2322 this.updateGearMenu_(); 2323 var currentEntry = this.getCurrentDirectoryEntry(); 2324 this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ? 2325 null : currentEntry; 2326 }; 2327 2328 FileManager.prototype.updateUnformattedVolumeStatus_ = function() { 2329 var volumeInfo = this.volumeManager_.getVolumeInfo( 2330 this.directoryModel_.getCurrentDirEntry()); 2331 2332 if (volumeInfo && volumeInfo.error) { 2333 this.dialogDom_.setAttribute('unformatted', ''); 2334 2335 var errorNode = this.dialogDom_.querySelector('#format-panel > .error'); 2336 if (volumeInfo.error == util.VolumeError.UNSUPPORTED_FILESYSTEM) { 2337 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING'); 2338 } else { 2339 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING'); 2340 } 2341 2342 // Update 'canExecute' for format command so the format button's disabled 2343 // property is properly set. 2344 if (this.commandHandler) 2345 this.commandHandler.updateAvailability(); 2346 } else { 2347 this.dialogDom_.removeAttribute('unformatted'); 2348 } 2349 }; 2350 2351 FileManager.prototype.findListItemForEvent_ = function(event) { 2352 return this.findListItemForNode_(event.touchedElement || event.srcElement); 2353 }; 2354 2355 FileManager.prototype.findListItemForNode_ = function(node) { 2356 var item = this.currentList_.getListItemAncestor(node); 2357 // TODO(serya): list should check that. 2358 return item && this.currentList_.isItem(item) ? item : null; 2359 }; 2360 2361 /** 2362 * Unload handler for the page. May be called manually for the file picker 2363 * dialog, because it closes by calling extension API functions that do not 2364 * return. 2365 * 2366 * TODO(hirono): This method is not called when Files.app is opend as a dialog 2367 * and is closed by the close button in the dialog frame. crbug.com/309967 2368 * @private 2369 */ 2370 FileManager.prototype.onUnload_ = function() { 2371 if (this.directoryModel_) 2372 this.directoryModel_.dispose(); 2373 if (this.volumeManager_) 2374 this.volumeManager_.dispose(); 2375 if (this.filePopup_ && 2376 this.filePopup_.contentWindow && 2377 this.filePopup_.contentWindow.unload) 2378 this.filePopup_.contentWindow.unload(true /* exiting */); 2379 if (this.progressCenterPanel_) 2380 this.backgroundPage_.background.progressCenter.removePanel( 2381 this.progressCenterPanel_); 2382 if (this.fileOperationManager_) { 2383 if (this.onCopyProgressBound_) { 2384 this.fileOperationManager_.removeEventListener( 2385 'copy-progress', this.onCopyProgressBound_); 2386 } 2387 if (this.onEntryChangedBound_) { 2388 this.fileOperationManager_.removeEventListener( 2389 'entry-changed', this.onEntryChangedBound_); 2390 } 2391 } 2392 window.closing = true; 2393 if (this.backgroundPage_ && util.platform.runningInBrowser()) 2394 this.backgroundPage_.background.tryClose(); 2395 }; 2396 2397 FileManager.prototype.initiateRename = function() { 2398 var item = this.currentList_.ensureLeadItemExists(); 2399 if (!item) 2400 return; 2401 var label = item.querySelector('.filename-label'); 2402 var input = this.renameInput_; 2403 2404 input.value = label.textContent; 2405 label.parentNode.setAttribute('renaming', ''); 2406 label.parentNode.appendChild(input); 2407 input.focus(); 2408 var selectionEnd = input.value.lastIndexOf('.'); 2409 if (selectionEnd == -1) { 2410 input.select(); 2411 } else { 2412 input.selectionStart = 0; 2413 input.selectionEnd = selectionEnd; 2414 } 2415 2416 // This has to be set late in the process so we don't handle spurious 2417 // blur events. 2418 input.currentEntry = this.currentList_.dataModel.item(item.listIndex); 2419 }; 2420 2421 /** 2422 * @type {Event} Key event. 2423 * @private 2424 */ 2425 FileManager.prototype.onRenameInputKeyDown_ = function(event) { 2426 if (!this.isRenamingInProgress()) 2427 return; 2428 2429 // Do not move selection or lead item in list during rename. 2430 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') { 2431 event.stopPropagation(); 2432 } 2433 2434 switch (util.getKeyModifiers(event) + event.keyCode) { 2435 case '27': // Escape 2436 this.cancelRename_(); 2437 event.preventDefault(); 2438 break; 2439 2440 case '13': // Enter 2441 this.commitRename_(); 2442 event.preventDefault(); 2443 break; 2444 } 2445 }; 2446 2447 /** 2448 * @type {Event} Blur event. 2449 * @private 2450 */ 2451 FileManager.prototype.onRenameInputBlur_ = function(event) { 2452 if (this.isRenamingInProgress() && !this.renameInput_.validation_) 2453 this.commitRename_(); 2454 }; 2455 2456 /** 2457 * @private 2458 */ 2459 FileManager.prototype.commitRename_ = function() { 2460 var input = this.renameInput_; 2461 var entry = input.currentEntry; 2462 var newName = input.value; 2463 2464 if (newName == entry.name) { 2465 this.cancelRename_(); 2466 return; 2467 } 2468 2469 var nameNode = this.findListItemForNode_(this.renameInput_). 2470 querySelector('.filename-label'); 2471 2472 input.validation_ = true; 2473 var validationDone = function(valid) { 2474 input.validation_ = false; 2475 // Alert dialog restores focus unless the item removed from DOM. 2476 if (this.document_.activeElement != input) 2477 this.cancelRename_(); 2478 if (!valid) 2479 return; 2480 2481 // Validation succeeded. Do renaming. 2482 2483 this.cancelRename_(); 2484 // Optimistically apply new name immediately to avoid flickering in 2485 // case of success. 2486 nameNode.textContent = newName; 2487 2488 util.rename( 2489 entry, newName, 2490 function(newEntry) { 2491 this.directoryModel_.onRenameEntry(entry, newEntry); 2492 }.bind(this), 2493 function(error) { 2494 // Write back to the old name. 2495 nameNode.textContent = entry.name; 2496 2497 // Show error dialog. 2498 var message; 2499 if (error.code == FileError.PATH_EXISTS_ERR || 2500 error.code == FileError.TYPE_MISMATCH_ERR) { 2501 // Check the existing entry is file or not. 2502 // 1) If the entry is a file: 2503 // a) If we get PATH_EXISTS_ERR, a file exists. 2504 // b) If we get TYPE_MISMATCH_ERR, a directory exists. 2505 // 2) If the entry is a directory: 2506 // a) If we get PATH_EXISTS_ERR, a directory exists. 2507 // b) If we get TYPE_MISMATCH_ERR, a file exists. 2508 message = strf( 2509 (entry.isFile && error.code == FileError.PATH_EXISTS_ERR) || 2510 (!entry.isFile && error.code == FileError.TYPE_MISMATCH_ERR) ? 2511 'FILE_ALREADY_EXISTS' : 2512 'DIRECTORY_ALREADY_EXISTS', 2513 newName); 2514 } else { 2515 message = strf('ERROR_RENAMING', entry.name, 2516 util.getFileErrorString(err.code)); 2517 } 2518 2519 this.alert.show(message); 2520 }.bind(this)); 2521 }; 2522 2523 // TODO(haruki): this.getCurrentDirectoryURL() might not return the actual 2524 // parent if the directory content is a search result. Fix it to do proper 2525 // validation. 2526 this.validateFileName_(this.getCurrentDirectoryURL(), 2527 newName, 2528 validationDone.bind(this)); 2529 }; 2530 2531 /** 2532 * @private 2533 */ 2534 FileManager.prototype.cancelRename_ = function() { 2535 this.renameInput_.currentEntry = null; 2536 2537 var parent = this.renameInput_.parentNode; 2538 if (parent) { 2539 parent.removeAttribute('renaming'); 2540 parent.removeChild(this.renameInput_); 2541 } 2542 }; 2543 2544 /** 2545 * @param {Event} Key event. 2546 * @private 2547 */ 2548 FileManager.prototype.onFilenameInputInput_ = function() { 2549 this.selectionHandler_.updateOkButton(); 2550 }; 2551 2552 /** 2553 * @param {Event} Key event. 2554 * @private 2555 */ 2556 FileManager.prototype.onFilenameInputKeyDown_ = function(event) { 2557 if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */) 2558 this.okButton_.click(); 2559 }; 2560 2561 /** 2562 * @param {Event} Focus event. 2563 * @private 2564 */ 2565 FileManager.prototype.onFilenameInputFocus_ = function(event) { 2566 var input = this.filenameInput_; 2567 2568 // On focus we want to select everything but the extension, but 2569 // Chrome will select-all after the focus event completes. We 2570 // schedule a timeout to alter the focus after that happens. 2571 setTimeout(function() { 2572 var selectionEnd = input.value.lastIndexOf('.'); 2573 if (selectionEnd == -1) { 2574 input.select(); 2575 } else { 2576 input.selectionStart = 0; 2577 input.selectionEnd = selectionEnd; 2578 } 2579 }, 0); 2580 }; 2581 2582 /** 2583 * @private 2584 */ 2585 FileManager.prototype.onScanStarted_ = function() { 2586 if (this.scanInProgress_) { 2587 this.table_.list.endBatchUpdates(); 2588 this.grid_.endBatchUpdates(); 2589 } 2590 2591 if (this.commandHandler) 2592 this.commandHandler.updateAvailability(); 2593 this.table_.list.startBatchUpdates(); 2594 this.grid_.startBatchUpdates(); 2595 this.scanInProgress_ = true; 2596 2597 this.scanUpdatedAtLeastOnceOrCompleted_ = false; 2598 if (this.scanCompletedTimer_) { 2599 clearTimeout(this.scanCompletedTimer_); 2600 this.scanCompletedTimer_ = null; 2601 } 2602 2603 if (this.scanUpdatedTimer_) { 2604 clearTimeout(this.scanUpdatedTimer_); 2605 this.scanUpdatedTimer_ = null; 2606 } 2607 2608 if (this.spinner_.hidden) { 2609 this.cancelSpinnerTimeout_(); 2610 this.showSpinnerTimeout_ = 2611 setTimeout(this.showSpinner_.bind(this, true), 500); 2612 } 2613 }; 2614 2615 /** 2616 * @private 2617 */ 2618 FileManager.prototype.onScanCompleted_ = function() { 2619 if (!this.scanInProgress_) { 2620 console.error('Scan-completed event recieved. But scan is not started.'); 2621 return; 2622 } 2623 2624 if (this.commandHandler) 2625 this.commandHandler.updateAvailability(); 2626 this.hideSpinnerLater_(); 2627 2628 if (this.scanUpdatedTimer_) { 2629 clearTimeout(this.scanUpdatedTimer_); 2630 this.scanUpdatedTimer_ = null; 2631 } 2632 2633 // To avoid flickering postpone updating the ui by a small amount of time. 2634 // There is a high chance, that metadata will be received within 50 ms. 2635 this.scanCompletedTimer_ = setTimeout(function() { 2636 // Check if batch updates are already finished by onScanUpdated_(). 2637 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { 2638 this.scanUpdatedAtLeastOnceOrCompleted_ = true; 2639 this.updateMiddleBarVisibility_(); 2640 } 2641 2642 this.scanInProgress_ = false; 2643 this.table_.list.endBatchUpdates(); 2644 this.grid_.endBatchUpdates(); 2645 this.scanCompletedTimer_ = null; 2646 }.bind(this), 50); 2647 }; 2648 2649 /** 2650 * @private 2651 */ 2652 FileManager.prototype.onScanUpdated_ = function() { 2653 if (!this.scanInProgress_) { 2654 console.error('Scan-updated event recieved. But scan is not started.'); 2655 return; 2656 } 2657 2658 if (this.scanUpdatedTimer_ || this.scanCompletedTimer_) 2659 return; 2660 2661 // Show contents incrementally by finishing batch updated, but only after 2662 // 200ms elapsed, to avoid flickering when it is not necessary. 2663 this.scanUpdatedTimer_ = setTimeout(function() { 2664 // We need to hide the spinner only once. 2665 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { 2666 this.scanUpdatedAtLeastOnceOrCompleted_ = true; 2667 this.hideSpinnerLater_(); 2668 this.updateMiddleBarVisibility_(); 2669 } 2670 2671 // Update the UI. 2672 if (this.scanInProgress_) { 2673 this.table_.list.endBatchUpdates(); 2674 this.grid_.endBatchUpdates(); 2675 this.table_.list.startBatchUpdates(); 2676 this.grid_.startBatchUpdates(); 2677 } 2678 this.scanUpdatedTimer_ = null; 2679 }.bind(this), 200); 2680 }; 2681 2682 /** 2683 * @private 2684 */ 2685 FileManager.prototype.onScanCancelled_ = function() { 2686 if (!this.scanInProgress_) { 2687 console.error('Scan-cancelled event recieved. But scan is not started.'); 2688 return; 2689 } 2690 2691 if (this.commandHandler) 2692 this.commandHandler.updateAvailability(); 2693 this.hideSpinnerLater_(); 2694 if (this.scanCompletedTimer_) { 2695 clearTimeout(this.scanCompletedTimer_); 2696 this.scanCompletedTimer_ = null; 2697 } 2698 if (this.scanUpdatedTimer_) { 2699 clearTimeout(this.scanUpdatedTimer_); 2700 this.scanUpdatedTimer_ = null; 2701 } 2702 // Finish unfinished batch updates. 2703 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { 2704 this.scanUpdatedAtLeastOnceOrCompleted_ = true; 2705 this.updateMiddleBarVisibility_(); 2706 } 2707 2708 this.scanInProgress_ = false; 2709 this.table_.list.endBatchUpdates(); 2710 this.grid_.endBatchUpdates(); 2711 }; 2712 2713 /** 2714 * Handle the 'rescan-completed' from the DirectoryModel. 2715 * @private 2716 */ 2717 FileManager.prototype.onRescanCompleted_ = function() { 2718 this.selectionHandler_.onFileSelectionChanged(); 2719 }; 2720 2721 /** 2722 * @private 2723 */ 2724 FileManager.prototype.cancelSpinnerTimeout_ = function() { 2725 if (this.showSpinnerTimeout_) { 2726 clearTimeout(this.showSpinnerTimeout_); 2727 this.showSpinnerTimeout_ = null; 2728 } 2729 }; 2730 2731 /** 2732 * @private 2733 */ 2734 FileManager.prototype.hideSpinnerLater_ = function() { 2735 this.cancelSpinnerTimeout_(); 2736 this.showSpinner_(false); 2737 }; 2738 2739 /** 2740 * @param {boolean} on True to show, false to hide. 2741 * @private 2742 */ 2743 FileManager.prototype.showSpinner_ = function(on) { 2744 if (on && this.directoryModel_ && this.directoryModel_.isScanning()) 2745 this.spinner_.hidden = false; 2746 2747 if (!on && (!this.directoryModel_ || 2748 !this.directoryModel_.isScanning() || 2749 this.directoryModel_.getFileList().length != 0)) { 2750 this.spinner_.hidden = true; 2751 } 2752 }; 2753 2754 FileManager.prototype.createNewFolder = function() { 2755 var defaultName = str('DEFAULT_NEW_FOLDER_NAME'); 2756 2757 // Find a name that doesn't exist in the data model. 2758 var files = this.directoryModel_.getFileList(); 2759 var hash = {}; 2760 for (var i = 0; i < files.length; i++) { 2761 var name = files.item(i).name; 2762 // Filtering names prevents from conflicts with prototype's names 2763 // and '__proto__'. 2764 if (name.substring(0, defaultName.length) == defaultName) 2765 hash[name] = 1; 2766 } 2767 2768 var baseName = defaultName; 2769 var separator = ''; 2770 var suffix = ''; 2771 var index = ''; 2772 2773 var advance = function() { 2774 separator = ' ('; 2775 suffix = ')'; 2776 index++; 2777 }; 2778 2779 var current = function() { 2780 return baseName + separator + index + suffix; 2781 }; 2782 2783 // Accessing hasOwnProperty is safe since hash properties filtered. 2784 while (hash.hasOwnProperty(current())) { 2785 advance(); 2786 } 2787 2788 var self = this; 2789 var list = self.currentList_; 2790 var tryCreate = function() { 2791 self.directoryModel_.createDirectory(current(), 2792 onSuccess, onError); 2793 }; 2794 2795 var onSuccess = function(entry) { 2796 metrics.recordUserAction('CreateNewFolder'); 2797 list.selectedItem = entry; 2798 self.initiateRename(); 2799 }; 2800 2801 var onError = function(error) { 2802 self.alert.show(strf('ERROR_CREATING_FOLDER', current(), 2803 util.getFileErrorString(error.code))); 2804 }; 2805 2806 tryCreate(); 2807 }; 2808 2809 /** 2810 * @param {Event} event Click event. 2811 * @private 2812 */ 2813 FileManager.prototype.onDetailViewButtonClick_ = function(event) { 2814 // Stop propagate and hide the menu manually, in order to prevent the focus 2815 // from being back to the button. (cf. http://crbug.com/248479) 2816 event.stopPropagation(); 2817 this.gearButton_.hideMenu(); 2818 2819 this.setListType(FileManager.ListType.DETAIL); 2820 this.currentList_.focus(); 2821 }; 2822 2823 /** 2824 * @param {Event} event Click event. 2825 * @private 2826 */ 2827 FileManager.prototype.onThumbnailViewButtonClick_ = function(event) { 2828 // Stop propagate and hide the menu manually, in order to prevent the focus 2829 // from being back to the button. (cf. http://crbug.com/248479) 2830 event.stopPropagation(); 2831 this.gearButton_.hideMenu(); 2832 2833 this.setListType(FileManager.ListType.THUMBNAIL); 2834 this.currentList_.focus(); 2835 }; 2836 2837 /** 2838 * KeyDown event handler for the document. 2839 * @param {Event} event Key event. 2840 * @private 2841 */ 2842 FileManager.prototype.onKeyDown_ = function(event) { 2843 if (event.srcElement === this.renameInput_) { 2844 // Ignore keydown handler in the rename input box. 2845 return; 2846 } 2847 2848 switch (util.getKeyModifiers(event) + event.keyCode) { 2849 case 'Ctrl-190': // Ctrl-. => Toggle filter files. 2850 this.fileFilter_.setFilterHidden( 2851 !this.fileFilter_.isFilterHiddenOn()); 2852 event.preventDefault(); 2853 return; 2854 2855 case '27': // Escape => Cancel dialog. 2856 if (this.dialogType != DialogType.FULL_PAGE) { 2857 // If there is nothing else for ESC to do, then cancel the dialog. 2858 event.preventDefault(); 2859 this.cancelButton_.click(); 2860 } 2861 break; 2862 } 2863 }; 2864 2865 /** 2866 * KeyDown event handler for the div#list-container element. 2867 * @param {Event} event Key event. 2868 * @private 2869 */ 2870 FileManager.prototype.onListKeyDown_ = function(event) { 2871 if (event.srcElement.tagName == 'INPUT') { 2872 // Ignore keydown handler in the rename input box. 2873 return; 2874 } 2875 2876 switch (util.getKeyModifiers(event) + event.keyCode) { 2877 case '8': // Backspace => Up one directory. 2878 event.preventDefault(); 2879 var path = this.getCurrentDirectory(); 2880 if (path && !PathUtil.isRootPath(path)) { 2881 var path = path.replace(/\/[^\/]+$/, ''); 2882 this.directoryModel_.changeDirectory(path); 2883 } 2884 break; 2885 2886 case '13': // Enter => Change directory or perform default action. 2887 // TODO(dgozman): move directory action to dispatchSelectionAction. 2888 var selection = this.getSelection(); 2889 if (selection.totalCount == 1 && 2890 selection.entries[0].isDirectory && 2891 !DialogType.isFolderDialog(this.dialogType)) { 2892 event.preventDefault(); 2893 this.onDirectoryAction_(selection.entries[0]); 2894 } else if (this.dispatchSelectionAction_()) { 2895 event.preventDefault(); 2896 } 2897 break; 2898 } 2899 2900 switch (event.keyIdentifier) { 2901 case 'Home': 2902 case 'End': 2903 case 'Up': 2904 case 'Down': 2905 case 'Left': 2906 case 'Right': 2907 // When navigating with keyboard we hide the distracting mouse hover 2908 // highlighting until the user moves the mouse again. 2909 this.setNoHover_(true); 2910 break; 2911 } 2912 }; 2913 2914 /** 2915 * Suppress/restore hover highlighting in the list container. 2916 * @param {boolean} on True to temporarity hide hover state. 2917 * @private 2918 */ 2919 FileManager.prototype.setNoHover_ = function(on) { 2920 if (on) { 2921 this.listContainer_.classList.add('nohover'); 2922 } else { 2923 this.listContainer_.classList.remove('nohover'); 2924 } 2925 }; 2926 2927 /** 2928 * KeyPress event handler for the div#list-container element. 2929 * @param {Event} event Key event. 2930 * @private 2931 */ 2932 FileManager.prototype.onListKeyPress_ = function(event) { 2933 if (event.srcElement.tagName == 'INPUT') { 2934 // Ignore keypress handler in the rename input box. 2935 return; 2936 } 2937 2938 if (event.ctrlKey || event.metaKey || event.altKey) 2939 return; 2940 2941 var now = new Date(); 2942 var char = String.fromCharCode(event.charCode).toLowerCase(); 2943 var text = now - this.textSearchState_.date > 1000 ? '' : 2944 this.textSearchState_.text; 2945 this.textSearchState_ = {text: text + char, date: now}; 2946 2947 this.doTextSearch_(); 2948 }; 2949 2950 /** 2951 * Mousemove event handler for the div#list-container element. 2952 * @param {Event} event Mouse event. 2953 * @private 2954 */ 2955 FileManager.prototype.onListMouseMove_ = function(event) { 2956 // The user grabbed the mouse, restore the hover highlighting. 2957 this.setNoHover_(false); 2958 }; 2959 2960 /** 2961 * Performs a 'text search' - selects a first list entry with name 2962 * starting with entered text (case-insensitive). 2963 * @private 2964 */ 2965 FileManager.prototype.doTextSearch_ = function() { 2966 var text = this.textSearchState_.text; 2967 if (!text) 2968 return; 2969 2970 var dm = this.directoryModel_.getFileList(); 2971 for (var index = 0; index < dm.length; ++index) { 2972 var name = dm.item(index).name; 2973 if (name.substring(0, text.length).toLowerCase() == text) { 2974 this.currentList_.selectionModel.selectedIndexes = [index]; 2975 return; 2976 } 2977 } 2978 2979 this.textSearchState_.text = ''; 2980 }; 2981 2982 /** 2983 * Handle a click of the cancel button. Closes the window. 2984 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 2985 * 2986 * @param {Event} event The click event. 2987 * @private 2988 */ 2989 FileManager.prototype.onCancel_ = function(event) { 2990 chrome.fileBrowserPrivate.cancelDialog(); 2991 this.onUnload_(); 2992 window.close(); 2993 }; 2994 2995 /** 2996 * Resolves selected file urls returned from an Open dialog. 2997 * 2998 * For drive files this involves some special treatment. 2999 * Starts getting drive files if needed. 3000 * 3001 * @param {Array.<string>} fileUrls Drive URLs. 3002 * @param {function(Array.<string>)} callback To be called with fixed URLs. 3003 * @private 3004 */ 3005 FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) { 3006 if (this.isOnDrive()) { 3007 chrome.fileBrowserPrivate.getDriveFiles( 3008 fileUrls, 3009 function(localPaths) { 3010 callback(fileUrls); 3011 }); 3012 } else { 3013 callback(fileUrls); 3014 } 3015 }; 3016 3017 /** 3018 * Closes this modal dialog with some files selected. 3019 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 3020 * @param {Object} selection Contains urls, filterIndex and multiple fields. 3021 * @private 3022 */ 3023 FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) { 3024 var self = this; 3025 function callback() { 3026 self.onUnload_(); 3027 window.close(); 3028 } 3029 if (selection.multiple) { 3030 chrome.fileBrowserPrivate.selectFiles( 3031 selection.urls, this.params_.shouldReturnLocalPath, callback); 3032 } else { 3033 var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE); 3034 chrome.fileBrowserPrivate.selectFile( 3035 selection.urls[0], selection.filterIndex, forOpening, 3036 this.params_.shouldReturnLocalPath, callback); 3037 } 3038 }; 3039 3040 /** 3041 * Tries to close this modal dialog with some files selected. 3042 * Performs preprocessing if needed (e.g. for Drive). 3043 * @param {Object} selection Contains urls, filterIndex and multiple fields. 3044 * @private 3045 */ 3046 FileManager.prototype.selectFilesAndClose_ = function(selection) { 3047 if (!this.isOnDrive() || 3048 this.dialogType == DialogType.SELECT_SAVEAS_FILE) { 3049 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); 3050 return; 3051 } 3052 3053 var shade = this.document_.createElement('div'); 3054 shade.className = 'shade'; 3055 var footer = this.dialogDom_.querySelector('.button-panel'); 3056 var progress = footer.querySelector('.progress-track'); 3057 progress.style.width = '0%'; 3058 var cancelled = false; 3059 3060 var progressMap = {}; 3061 var filesStarted = 0; 3062 var filesTotal = selection.urls.length; 3063 for (var index = 0; index < selection.urls.length; index++) { 3064 progressMap[selection.urls[index]] = -1; 3065 } 3066 var lastPercent = 0; 3067 var bytesTotal = 0; 3068 var bytesDone = 0; 3069 3070 var onFileTransfersUpdated = function(statusList) { 3071 for (var index = 0; index < statusList.length; index++) { 3072 var status = statusList[index]; 3073 var escaped = encodeURI(status.fileUrl); 3074 if (!(escaped in progressMap)) continue; 3075 if (status.total == -1) continue; 3076 3077 var old = progressMap[escaped]; 3078 if (old == -1) { 3079 // -1 means we don't know file size yet. 3080 bytesTotal += status.total; 3081 filesStarted++; 3082 old = 0; 3083 } 3084 bytesDone += status.processed - old; 3085 progressMap[escaped] = status.processed; 3086 } 3087 3088 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal; 3089 // For files we don't have information about, assume the progress is zero. 3090 percent = percent * filesStarted / filesTotal * 100; 3091 // Do not decrease the progress. This may happen, if first downloaded 3092 // file is small, and the second one is large. 3093 lastPercent = Math.max(lastPercent, percent); 3094 progress.style.width = lastPercent + '%'; 3095 }.bind(this); 3096 3097 var setup = function() { 3098 this.document_.querySelector('.dialog-container').appendChild(shade); 3099 setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100); 3100 footer.setAttribute('progress', 'progress'); 3101 this.cancelButton_.removeEventListener('click', this.onCancelBound_); 3102 this.cancelButton_.addEventListener('click', onCancel); 3103 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener( 3104 onFileTransfersUpdated); 3105 }.bind(this); 3106 3107 var cleanup = function() { 3108 shade.parentNode.removeChild(shade); 3109 footer.removeAttribute('progress'); 3110 this.cancelButton_.removeEventListener('click', onCancel); 3111 this.cancelButton_.addEventListener('click', this.onCancelBound_); 3112 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( 3113 onFileTransfersUpdated); 3114 }.bind(this); 3115 3116 var onCancel = function() { 3117 cancelled = true; 3118 // According to API cancel may fail, but there is no proper UI to reflect 3119 // this. So, we just silently assume that everything is cancelled. 3120 chrome.fileBrowserPrivate.cancelFileTransfers( 3121 selection.urls, function(response) {}); 3122 cleanup(); 3123 }.bind(this); 3124 3125 var onResolved = function(resolvedUrls) { 3126 if (cancelled) return; 3127 cleanup(); 3128 selection.urls = resolvedUrls; 3129 // Call next method on a timeout, as it's unsafe to 3130 // close a window from a callback. 3131 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); 3132 }.bind(this); 3133 3134 var onProperties = function(properties) { 3135 for (var i = 0; i < properties.length; i++) { 3136 if (!properties[i] || properties[i].present) { 3137 // For files already in GCache, we don't get any transfer updates. 3138 filesTotal--; 3139 } 3140 } 3141 this.resolveSelectResults_(selection.urls, onResolved); 3142 }.bind(this); 3143 3144 setup(); 3145 3146 // TODO(mtomasz): Use Entry instead of URLs, if possible. 3147 util.URLsToEntries(selection.urls, function(entries) { 3148 this.metadataCache_.get(entries, 'drive', onProperties); 3149 }.bind(this)); 3150 }; 3151 3152 /** 3153 * Handle a click of the ok button. 3154 * 3155 * The ok button has different UI labels depending on the type of dialog, but 3156 * in code it's always referred to as 'ok'. 3157 * 3158 * @param {Event} event The click event. 3159 * @private 3160 */ 3161 FileManager.prototype.onOk_ = function(event) { 3162 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) { 3163 // Save-as doesn't require a valid selection from the list, since 3164 // we're going to take the filename from the text input. 3165 var filename = this.filenameInput_.value; 3166 if (!filename) 3167 throw new Error('Missing filename!'); 3168 3169 var directory = this.getCurrentDirectoryEntry(); 3170 var currentDirUrl = directory.toURL(); 3171 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/') 3172 currentDirUrl += '/'; 3173 this.validateFileName_(currentDirUrl, filename, function(isValid) { 3174 if (!isValid) 3175 return; 3176 3177 if (util.isFakeEntry(directory)) { 3178 // Can't save a file into a fake directory. 3179 return; 3180 } 3181 3182 var selectFileAndClose = function() { 3183 this.selectFilesAndClose_({ 3184 urls: [currentDirUrl + encodeURIComponent(filename)], 3185 multiple: false, 3186 filterIndex: this.getSelectedFilterIndex_(filename) 3187 }); 3188 }.bind(this); 3189 3190 directory.getFile( 3191 filename, {create: false}, 3192 function(entry) { 3193 // An existing file is found. Show confirmation dialog to 3194 // overwrite it. If the user select "OK" on the dialog, save it. 3195 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename), 3196 selectFileAndClose); 3197 }.bind(this), 3198 function(error) { 3199 if (error.code == FileError.NOT_FOUND_ERR) { 3200 // The file does not exist, so it should be ok to create a 3201 // new file. 3202 selectFileAndClose(); 3203 return; 3204 } 3205 if (error.code == FileError.TYPE_MISMATCH_ERR) { 3206 // An directory is found. 3207 // Do not allow to overwrite directory. 3208 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename)); 3209 return; 3210 } 3211 3212 // Unexpected error. 3213 console.error('File save failed: ' + error.code); 3214 }.bind(this)); 3215 }.bind(this)); 3216 return; 3217 } 3218 3219 var files = []; 3220 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes; 3221 3222 if (DialogType.isFolderDialog(this.dialogType) && 3223 selectedIndexes.length == 0) { 3224 var url = this.getCurrentDirectoryURL(); 3225 var singleSelection = { 3226 urls: [url], 3227 multiple: false, 3228 filterIndex: this.getSelectedFilterIndex_() 3229 }; 3230 this.selectFilesAndClose_(singleSelection); 3231 return; 3232 } 3233 3234 // All other dialog types require at least one selected list item. 3235 // The logic to control whether or not the ok button is enabled should 3236 // prevent us from ever getting here, but we sanity check to be sure. 3237 if (!selectedIndexes.length) 3238 throw new Error('Nothing selected!'); 3239 3240 var dm = this.directoryModel_.getFileList(); 3241 for (var i = 0; i < selectedIndexes.length; i++) { 3242 var entry = dm.item(selectedIndexes[i]); 3243 if (!entry) { 3244 console.error('Error locating selected file at index: ' + i); 3245 continue; 3246 } 3247 3248 files.push(entry.toURL()); 3249 } 3250 3251 // Multi-file selection has no other restrictions. 3252 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) { 3253 var multipleSelection = { 3254 urls: files, 3255 multiple: true 3256 }; 3257 this.selectFilesAndClose_(multipleSelection); 3258 return; 3259 } 3260 3261 // Everything else must have exactly one. 3262 if (files.length > 1) 3263 throw new Error('Too many files selected!'); 3264 3265 var selectedEntry = dm.item(selectedIndexes[0]); 3266 3267 if (DialogType.isFolderDialog(this.dialogType)) { 3268 if (!selectedEntry.isDirectory) 3269 throw new Error('Selected entry is not a folder!'); 3270 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) { 3271 if (!selectedEntry.isFile) 3272 throw new Error('Selected entry is not a file!'); 3273 } 3274 3275 var singleSelection = { 3276 urls: [files[0]], 3277 multiple: false, 3278 filterIndex: this.getSelectedFilterIndex_() 3279 }; 3280 this.selectFilesAndClose_(singleSelection); 3281 }; 3282 3283 /** 3284 * Verifies the user entered name for file or folder to be created or 3285 * renamed to. Name restrictions must correspond to File API restrictions 3286 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is 3287 * out of date (spec is 3288 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to 3289 * be fixed. Shows message box if the name is invalid. 3290 * 3291 * It also verifies if the name length is in the limit of the filesystem. 3292 * 3293 * @param {string} parentUrl The URL of the parent directory entry. 3294 * @param {string} name New file or folder name. 3295 * @param {function} onDone Function to invoke when user closes the 3296 * warning box or immediatelly if file name is correct. If the name was 3297 * valid it is passed true, and false otherwise. 3298 * @private 3299 */ 3300 FileManager.prototype.validateFileName_ = function(parentUrl, name, onDone) { 3301 var msg; 3302 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name); 3303 if (testResult) { 3304 msg = strf('ERROR_INVALID_CHARACTER', testResult[0]); 3305 } else if (/^\s*$/i.test(name)) { 3306 msg = str('ERROR_WHITESPACE_NAME'); 3307 } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) { 3308 msg = str('ERROR_RESERVED_NAME'); 3309 } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') { 3310 msg = str('ERROR_HIDDEN_NAME'); 3311 } 3312 3313 if (msg) { 3314 this.alert.show(msg, function() { 3315 onDone(false); 3316 }); 3317 return; 3318 } 3319 3320 var self = this; 3321 chrome.fileBrowserPrivate.validatePathNameLength( 3322 parentUrl, name, function(valid) { 3323 if (!valid) { 3324 self.alert.show(str('ERROR_LONG_NAME'), 3325 function() { onDone(false); }); 3326 } else { 3327 onDone(true); 3328 } 3329 }); 3330 }; 3331 3332 /** 3333 * Handler invoked on preference setting in drive context menu. 3334 * 3335 * @param {string} pref The preference to alter. 3336 * @param {boolean} inverted Invert the value if true. 3337 * @param {Event} event The click event. 3338 * @private 3339 */ 3340 FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) { 3341 var newValue = !event.target.hasAttribute('checked'); 3342 if (newValue) 3343 event.target.setAttribute('checked', 'checked'); 3344 else 3345 event.target.removeAttribute('checked'); 3346 3347 var changeInfo = {}; 3348 changeInfo[pref] = inverted ? !newValue : newValue; 3349 chrome.fileBrowserPrivate.setPreferences(changeInfo); 3350 }; 3351 3352 /** 3353 * Invoked when the search box is changed. 3354 * 3355 * @param {Event} event The changed event. 3356 * @private 3357 */ 3358 FileManager.prototype.onSearchBoxUpdate_ = function(event) { 3359 var searchString = this.searchBox_.value; 3360 3361 if (this.isOnDrive()) { 3362 // When the search text is changed, finishes the search and showes back 3363 // the last directory by passing an empty string to 3364 // {@code DirectoryModel.search()}. 3365 if (this.directoryModel_.isSearching() && 3366 this.lastSearchQuery_ != searchString) { 3367 this.doSearch(''); 3368 } 3369 3370 // On drive, incremental search is not invoked since we have an auto- 3371 // complete suggestion instead. 3372 return; 3373 } 3374 3375 this.search_(searchString); 3376 }; 3377 3378 /** 3379 * Handle the search clear button click. 3380 * @private 3381 */ 3382 FileManager.prototype.onSearchClearButtonClick_ = function() { 3383 this.ui_.searchBox.clear(); 3384 this.onSearchBoxUpdate_(); 3385 }; 3386 3387 /** 3388 * Search files and update the list with the search result. 3389 * 3390 * @param {string} searchString String to be searched with. 3391 * @private 3392 */ 3393 FileManager.prototype.search_ = function(searchString) { 3394 var noResultsDiv = this.document_.getElementById('no-search-results'); 3395 3396 var reportEmptySearchResults = function() { 3397 if (this.directoryModel_.getFileList().length === 0) { 3398 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags, 3399 // hence we escapes |searchString| here. 3400 var html = strf('SEARCH_NO_MATCHING_FILES_HTML', 3401 util.htmlEscape(searchString)); 3402 noResultsDiv.innerHTML = html; 3403 noResultsDiv.setAttribute('show', 'true'); 3404 } else { 3405 noResultsDiv.removeAttribute('show'); 3406 } 3407 }; 3408 3409 var hideNoResultsDiv = function() { 3410 noResultsDiv.removeAttribute('show'); 3411 }; 3412 3413 this.doSearch(searchString, 3414 reportEmptySearchResults.bind(this), 3415 hideNoResultsDiv.bind(this)); 3416 }; 3417 3418 /** 3419 * Performs search and displays results. 3420 * 3421 * @param {string} query Query that will be searched for. 3422 * @param {function()=} opt_onSearchRescan Function that will be called when 3423 * the search directory is rescanned (i.e. search results are displayed). 3424 * @param {function()=} opt_onClearSearch Function to be called when search 3425 * state gets cleared. 3426 */ 3427 FileManager.prototype.doSearch = function( 3428 searchString, opt_onSearchRescan, opt_onClearSearch) { 3429 var onSearchRescan = opt_onSearchRescan || function() {}; 3430 var onClearSearch = opt_onClearSearch || function() {}; 3431 3432 this.lastSearchQuery_ = searchString; 3433 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch); 3434 }; 3435 3436 /** 3437 * Requests autocomplete suggestions for files on Drive. 3438 * Once the suggestions are returned, the autocomplete popup will show up. 3439 * 3440 * @param {string} query The text to autocomplete from. 3441 * @private 3442 */ 3443 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) { 3444 query = query.trimLeft(); 3445 3446 // Only Drive supports auto-compelete 3447 if (!this.isOnDrive()) 3448 return; 3449 3450 // Remember the most recent query. If there is an other request in progress, 3451 // then it's result will be discarded and it will call a new request for 3452 // this query. 3453 this.lastAutocompleteQuery_ = query; 3454 if (this.autocompleteSuggestionsBusy_) 3455 return; 3456 3457 // The autocomplete list should be resized and repositioned here as the 3458 // search box is resized when it's focused. 3459 this.autocompleteList_.syncWidthAndPositionToInput(); 3460 3461 if (!query) { 3462 this.autocompleteList_.suggestions = []; 3463 return; 3464 } 3465 3466 var headerItem = {isHeaderItem: true, searchQuery: query}; 3467 if (!this.autocompleteList_.dataModel || 3468 this.autocompleteList_.dataModel.length == 0) 3469 this.autocompleteList_.suggestions = [headerItem]; 3470 else 3471 // Updates only the head item to prevent a flickering on typing. 3472 this.autocompleteList_.dataModel.splice(0, 1, headerItem); 3473 3474 this.autocompleteSuggestionsBusy_ = true; 3475 3476 var searchParams = { 3477 'query': query, 3478 'types': 'ALL', 3479 'maxResults': 4 3480 }; 3481 chrome.fileBrowserPrivate.searchDriveMetadata( 3482 searchParams, 3483 function(suggestions) { 3484 this.autocompleteSuggestionsBusy_ = false; 3485 3486 // Discard results for previous requests and fire a new search 3487 // for the most recent query. 3488 if (query != this.lastAutocompleteQuery_) { 3489 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_); 3490 return; 3491 } 3492 3493 // Keeps the items in the suggestion list. 3494 this.autocompleteList_.suggestions = [headerItem].concat(suggestions); 3495 }.bind(this)); 3496 }; 3497 3498 /** 3499 * Opens the currently selected suggestion item. 3500 * @private 3501 */ 3502 FileManager.prototype.openAutocompleteSuggestion_ = function() { 3503 var selectedItem = this.autocompleteList_.selectedItem; 3504 3505 // If the entry is the search item or no entry is selected, just change to 3506 // the search result. 3507 if (!selectedItem || selectedItem.isHeaderItem) { 3508 var query = selectedItem ? 3509 selectedItem.searchQuery : this.searchBox_.value; 3510 this.search_(query); 3511 return; 3512 } 3513 3514 var entry = selectedItem.entry; 3515 // If the entry is a directory, just change the directory. 3516 if (entry.isDirectory) { 3517 this.onDirectoryAction_(entry); 3518 return; 3519 } 3520 3521 var entries = [entry]; 3522 var self = this; 3523 3524 // To open a file, first get the mime type. 3525 this.metadataCache_.get(entries, 'drive', function(props) { 3526 var mimeType = props[0].contentMimeType || ''; 3527 var mimeTypes = [mimeType]; 3528 var openIt = function() { 3529 if (self.dialogType == DialogType.FULL_PAGE) { 3530 var tasks = new FileTasks(self); 3531 tasks.init(entries, mimeTypes); 3532 tasks.executeDefault(); 3533 } else { 3534 self.onOk_(); 3535 } 3536 }; 3537 3538 // Change the current directory to the directory that contains the 3539 // selected file. Note that this is necessary for an image or a video, 3540 // which should be opened in the gallery mode, as the gallery mode 3541 // requires the entry to be in the current directory model. For 3542 // consistency, the current directory is always changed regardless of 3543 // the file type. 3544 entry.getParent(function(parent) { 3545 var onDirectoryChanged = function(event) { 3546 self.directoryModel_.removeEventListener('scan-completed', 3547 onDirectoryChanged); 3548 self.directoryModel_.selectEntry(entry); 3549 openIt(); 3550 }; 3551 // changeDirectory() returns immediately. We should wait until the 3552 // directory scan is complete. 3553 self.directoryModel_.addEventListener('scan-completed', 3554 onDirectoryChanged); 3555 self.directoryModel_.changeDirectory( 3556 parent.fullPath, 3557 function() { 3558 // Remove the listner if the change directory failed. 3559 self.directoryModel_.removeEventListener('scan-completed', 3560 onDirectoryChanged); 3561 }); 3562 }); 3563 }); 3564 }; 3565 3566 FileManager.prototype.decorateSplitter = function(splitterElement) { 3567 var self = this; 3568 3569 var Splitter = cr.ui.Splitter; 3570 3571 var customSplitter = cr.ui.define('div'); 3572 3573 customSplitter.prototype = { 3574 __proto__: Splitter.prototype, 3575 3576 handleSplitterDragStart: function(e) { 3577 Splitter.prototype.handleSplitterDragStart.apply(this, arguments); 3578 this.ownerDocument.documentElement.classList.add('col-resize'); 3579 }, 3580 3581 handleSplitterDragMove: function(deltaX) { 3582 Splitter.prototype.handleSplitterDragMove.apply(this, arguments); 3583 self.onResize_(); 3584 }, 3585 3586 handleSplitterDragEnd: function(e) { 3587 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments); 3588 this.ownerDocument.documentElement.classList.remove('col-resize'); 3589 } 3590 }; 3591 3592 customSplitter.decorate(splitterElement); 3593 }; 3594 3595 /** 3596 * Updates default action menu item to match passed taskItem (icon, 3597 * label and action). 3598 * 3599 * @param {Object} defaultItem - taskItem to match. 3600 * @param {boolean} isMultiple - if multiple tasks available. 3601 */ 3602 FileManager.prototype.updateContextMenuActionItems = function(defaultItem, 3603 isMultiple) { 3604 if (defaultItem) { 3605 if (defaultItem.iconType) { 3606 this.defaultActionMenuItem_.style.backgroundImage = ''; 3607 this.defaultActionMenuItem_.setAttribute('file-type-icon', 3608 defaultItem.iconType); 3609 } else if (defaultItem.iconUrl) { 3610 this.defaultActionMenuItem_.style.backgroundImage = 3611 'url(' + defaultItem.iconUrl + ')'; 3612 } else { 3613 this.defaultActionMenuItem_.style.backgroundImage = ''; 3614 } 3615 3616 this.defaultActionMenuItem_.label = defaultItem.title; 3617 this.defaultActionMenuItem_.disabled = !!defaultItem.disabled; 3618 this.defaultActionMenuItem_.taskId = defaultItem.taskId; 3619 } 3620 3621 var defaultActionSeparator = 3622 this.dialogDom_.querySelector('#default-action-separator'); 3623 3624 this.openWithCommand_.canExecuteChange(); 3625 this.openWithCommand_.setHidden(!(defaultItem && isMultiple)); 3626 this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled; 3627 3628 this.defaultActionMenuItem_.hidden = !defaultItem; 3629 defaultActionSeparator.hidden = !defaultItem; 3630 }; 3631 3632 /** 3633 * Window beforeunload handler. 3634 * @return {string} Message to show. Ignored when running as a packaged app. 3635 * @private 3636 */ 3637 FileManager.prototype.onBeforeUnload_ = function() { 3638 if (this.filePopup_ && 3639 this.filePopup_.contentWindow && 3640 this.filePopup_.contentWindow.beforeunload) { 3641 // The gallery might want to prevent the unload if it is busy. 3642 return this.filePopup_.contentWindow.beforeunload(); 3643 } 3644 return null; 3645 }; 3646 3647 /** 3648 * @return {FileSelection} Selection object. 3649 */ 3650 FileManager.prototype.getSelection = function() { 3651 return this.selectionHandler_.selection; 3652 }; 3653 3654 /** 3655 * @return {ArrayDataModel} File list. 3656 */ 3657 FileManager.prototype.getFileList = function() { 3658 return this.directoryModel_.getFileList(); 3659 }; 3660 3661 /** 3662 * @return {cr.ui.List} Current list object. 3663 */ 3664 FileManager.prototype.getCurrentList = function() { 3665 return this.currentList_; 3666 }; 3667 3668 /** 3669 * Retrieve the preferences of the files.app. This method caches the result 3670 * and returns it unless opt_update is true. 3671 * @param {function(Object.<string, *>)} callback Callback to get the 3672 * preference. 3673 * @param {boolean=} opt_update If is's true, don't use the cache and 3674 * retrieve latest preference. Default is false. 3675 * @private 3676 */ 3677 FileManager.prototype.getPreferences_ = function(callback, opt_update) { 3678 if (!opt_update && this.preferences_ !== undefined) { 3679 callback(this.preferences_); 3680 return; 3681 } 3682 3683 chrome.fileBrowserPrivate.getPreferences(function(prefs) { 3684 this.preferences_ = prefs; 3685 callback(prefs); 3686 }.bind(this)); 3687 }; 3688 })(); 3689