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