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