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