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.
5 // This variable is checked in SelectFileDialogExtensionBrowserTest.
9 * Count uncaught exceptions.
11 window
.onerror = function() { JSErrorCount
++ };
14 * FileManager constructor.
16 * FileManager objects encapsulate the functionality of the file selector
17 * dialogs, as well as the full screen file manager application (though the
18 * latter is not yet implemented).
21 * @param {HTMLElement} dialogDom The DOM node containing the prototypical
24 function FileManager(dialogDom
) {
25 this.dialogDom_
= dialogDom
;
26 this.filesystem_
= null;
28 if (window
.appState
) {
29 this.params_
= window
.appState
.params
|| {};
30 this.defaultPath
= window
.appState
.defaultPath
;
33 this.params_
= location
.search
?
34 JSON
.parse(decodeURIComponent(location
.search
.substr(1))) :
36 this.defaultPath
= this.params_
.defaultPath
;
38 this.listType_
= null;
39 this.showDelayTimeout_
= null;
41 this.filesystemObserverId_
= null;
42 this.gdataObserverId_
= null;
44 this.document_
= dialogDom
.ownerDocument
;
45 this.dialogType
= this.params_
.type
|| DialogType
.FULL_PAGE
;
46 this.startupPrefName_
= 'file-manager-' + this.dialogType
;
48 // Optional list of file types.
49 this.fileTypes_
= this.params_
.typeList
|| [];
50 metrics
.recordEnum('Create', this.dialogType
,
51 [DialogType
.SELECT_FOLDER
,
52 DialogType
.SELECT_SAVEAS_FILE
,
53 DialogType
.SELECT_OPEN_FILE
,
54 DialogType
.SELECT_OPEN_MULTI_FILE
,
55 DialogType
.FULL_PAGE
]);
57 this.selectionHandler_
= null;
59 this.metadataCache_
= MetadataCache
.createFull();
60 this.initFileSystem_();
61 this.volumeManager_
= VolumeManager
.getInstance();
63 this.initDialogType_();
67 * Maximum delay in milliseconds for updating thumbnails in the bottom panel
68 * to mitigate flickering. If images load faster then the delay they replace
69 * old images smoothly. On the other hand we don't want to keep old images
72 FileManager
.THUMBNAIL_SHOW_DELAY
= 100;
74 FileManager
.prototype = {
75 __proto__
: cr
.EventTarget
.prototype
79 * Unload the file manager.
80 * Used by background.js (when running in the packaged mode).
83 fileManager
.onBeforeUnload_();
84 fileManager
.onUnload_();
88 * List of dialog types.
90 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
91 * FULL_PAGE which is specific to this code.
96 SELECT_FOLDER
: 'folder',
97 SELECT_SAVEAS_FILE
: 'saveas-file',
98 SELECT_OPEN_FILE
: 'open-file',
99 SELECT_OPEN_MULTI_FILE
: 'open-multi-file',
100 FULL_PAGE
: 'full-page'
104 * @param {string} type Dialog type.
105 * @return {boolean} Whether the type is modal.
107 DialogType
.isModal = function(type
) {
108 return type
== DialogType
.SELECT_FOLDER
||
109 type
== DialogType
.SELECT_SAVEAS_FILE
||
110 type
== DialogType
.SELECT_OPEN_FILE
||
111 type
== DialogType
.SELECT_OPEN_MULTI_FILE
;
114 // Anonymous "namespace".
117 // Private variables and helper functions.
120 * Location of the page to buy more storage for Google Drive.
122 FileManager
.GOOGLE_DRIVE_BUY_STORAGE
=
123 'https://www.google.com/settings/storage';
126 * Location of Google Drive specific help.
128 FileManager
.GOOGLE_DRIVE_HELP
=
129 'https://support.google.com/chromeos/?p=filemanager_drivehelp';
132 * Location of Google Drive specific help.
134 FileManager
.GOOGLE_DRIVE_ROOT
= 'https://drive.google.com';
137 * Number of milliseconds in a day.
139 var MILLISECONDS_IN_DAY
= 24 * 60 * 60 * 1000;
142 * Some UI elements react on a single click and standard double click handling
143 * leads to confusing results. We ignore a second click if it comes soon
146 var DOUBLE_CLICK_TIMEOUT
= 200;
148 function removeChildren(element
) {
149 element
.textContent
= '';
154 FileManager
.ListType
= {
160 * FileWatcher that also watches for metadata changes.
161 * @extends {FileWatcher}
163 FileManager
.MetadataFileWatcher = function(fileManager
) {
164 FileWatcher
.call(this,
165 fileManager
.filesystem_
.root
,
166 fileManager
.directoryModel_
,
167 fileManager
.volumeManager_
);
168 this.metadataCache_
= fileManager
.metadataCache_
;
170 this.filesystemChangeHandler_
=
171 fileManager
.updateMetadataInUI_
.bind(fileManager
, 'filesystem');
172 this.thumbnailChangeHandler_
=
173 fileManager
.updateMetadataInUI_
.bind(fileManager
, 'thumbnail');
174 this.gdataChangeHandler_
=
175 fileManager
.updateMetadataInUI_
.bind(fileManager
, 'gdata');
177 var dm
= fileManager
.directoryModel_
;
178 this.internalChangeHandler_
= dm
.rescan
.bind(dm
);
180 this.filesystemObserverId_
= null;
181 this.thumbnailObserverId_
= null;
182 this.gdataObserverId_
= null;
183 this.internalObserverId_
= null;
186 FileManager
.MetadataFileWatcher
.prototype.__proto__
= FileWatcher
.prototype;
189 * Changed metadata observers for the new directory.
191 * @param {?DirectoryEntry} entry New watched directory entry.
194 FileManager
.MetadataFileWatcher
.prototype.changeWatchedEntry = function(
196 FileWatcher
.prototype.changeWatchedEntry
.call(this, entry
);
198 if (this.filesystemObserverId_
)
199 this.metadataCache_
.removeObserver(this.filesystemObserverId_
);
200 if (this.thumbnailObserverId_
)
201 this.metadataCache_
.removeObserver(this.thumbnailObserverId_
);
202 if (this.gdataObserverId_
)
203 this.metadataCache_
.removeObserver(this.gdataObserverId_
);
204 this.filesystemObserverId_
= null;
205 this.gdataObserverId_
= null;
206 this.internalObserverId_
= null;
210 this.filesystemObserverId_
= this.metadataCache_
.addObserver(
212 MetadataCache
.CHILDREN
,
214 this.filesystemChangeHandler_
);
216 this.thumbnailObserverId_
= this.metadataCache_
.addObserver(
218 MetadataCache
.CHILDREN
,
220 this.thumbnailChangeHandler_
);
222 if (PathUtil
.getRootType(entry
.fullPath
) === RootType
.GDATA
) {
223 this.gdataObserverId_
= this.metadataCache_
.addObserver(
225 MetadataCache
.CHILDREN
,
227 this.gdataChangeHandler_
);
230 this.internalObserverId_
= this.metadataCache_
.addObserver(
232 MetadataCache
.CHILDREN
,
234 this.internalChangeHandler_
);
240 FileManager
.MetadataFileWatcher
.prototype.onFileInWatchedDirectoryChanged
=
242 FileWatcher
.prototype.onFileInWatchedDirectoryChanged
.apply(this);
243 this.metadataCache_
.resumeRefresh(this.getWatchedDirectoryEntry().toURL());
247 * Load translated strings.
249 FileManager
.initStrings = function(callback
) {
250 chrome
.fileBrowserPrivate
.getStrings(function(strings
) {
251 loadTimeData
.data
= strings
;
258 * FileManager initially created hidden to prevent flickering.
259 * When DOM is almost constructed it need to be shown. Cancels
262 FileManager
.prototype.show_ = function() {
263 if (this.showDelayTimeout_
) {
264 clearTimeout(this.showDelayTimeout_
);
265 showDelayTimeout_
= null;
267 this.dialogDom_
.classList
.add('loaded');
271 * If initialization code think that right after initialization
272 * something going to be shown instead of just a file list (like Gallery)
273 * it may delay show to prevent flickering. However initialization may take
274 * significant time and we don't want to keep it hidden for too long.
275 * So it will be shown not more than in 0.5 sec. If initialization completed
276 * the page must show immediatelly.
278 * @param {number} delay In milliseconds.
280 FileManager
.prototype.delayShow_ = function(delay
) {
281 if (!this.showDelayTimeout_
) {
282 this.showDelayTimeout_
= setTimeout(function() {
283 this.showDelayTimeout_
= null;
285 }.bind(this), delay
);
292 * Request local file system, resolve roots and init_ after that.
295 FileManager
.prototype.initFileSystem_ = function() {
296 util
.installFileErrorToString();
297 // Replace the default unit in util to translated unit.
298 util
.UNITS
= [str('SIZE_KB'),
304 metrics
.startInterval('Load.FileSystem');
308 var viewOptions
= {};
310 if (--downcount
== 0)
311 self
.init_(viewOptions
);
314 chrome
.fileBrowserPrivate
.requestLocalFileSystem(function(filesystem
) {
315 metrics
.recordInterval('Load.FileSystem');
316 self
.filesystem_
= filesystem
;
320 // GDATA preferences should be initialized before creating DirectoryModel
321 // to tot rebuild the roots list.
322 this.updateNetworkStateAndPreferences_(done
);
324 util
.platform
.getPreference(this.startupPrefName_
, function(value
) {
325 // Load the global default options.
327 viewOptions
= JSON
.parse(value
);
329 // Override with window-specific options.
330 if (window
.appState
&& window
.appState
.viewOptions
) {
331 for (var key
in window
.appState
.viewOptions
) {
332 if (window
.appState
.viewOptions
.hasOwnProperty(key
))
333 viewOptions
[key
] = window
.appState
.viewOptions
[key
];
341 * @param {Object} prefs Preferences.
342 * Continue initializing the file manager after resolving roots.
344 FileManager
.prototype.init_ = function(prefs
) {
345 metrics
.startInterval('Load.DOM');
347 // PyAuto tests monitor this state by polling this variable
348 this.__defineGetter__('workerInitialized_', function() {
349 return this.metadataCache_
.isInitialized();
352 this.initDateTimeFormatters_();
354 this.table_
.startBatchUpdates();
355 this.grid_
.startBatchUpdates();
357 this.initFileList_(prefs
);
359 this.bannersController_
= new FileListBannerController(
360 this.directoryModel_
, this.volumeManager_
, this.document_
);
361 this.bannersController_
.addEventListener('relayout',
362 this.onResize_
.bind(this));
364 if (!util
.platform
.v2()) {
365 window
.addEventListener('popstate', this.onPopState_
.bind(this));
366 window
.addEventListener('unload', this.onUnload_
.bind(this));
367 window
.addEventListener('beforeunload', this.onBeforeUnload_
.bind(this));
370 var dm
= this.directoryModel_
;
371 dm
.addEventListener('directory-changed',
372 this.onDirectoryChanged_
.bind(this));
374 dm
.addEventListener('begin-update-files', function() {
375 self
.currentList_
.startBatchUpdates();
377 dm
.addEventListener('end-update-files', function() {
378 self
.restoreItemBeingRenamed_();
379 self
.currentList_
.endBatchUpdates();
381 dm
.addEventListener('scan-started', this.onScanStarted_
.bind(this));
382 dm
.addEventListener('scan-completed', this.showSpinner_
.bind(this, false));
383 dm
.addEventListener('scan-cancelled', this.hideSpinnerLater_
.bind(this));
384 dm
.addEventListener('scan-completed',
385 this.refreshCurrentDirectoryMetadata_
.bind(this));
386 dm
.addEventListener('rescan-completed',
387 this.refreshCurrentDirectoryMetadata_
.bind(this));
389 this.directoryModel_
.sortFileList(
390 prefs
.sortField
|| 'modificationTime',
391 prefs
.sortDirection
|| 'desc');
393 var stateChangeHandler
=
394 this.onNetworkStateOrPreferencesChanged_
.bind(this);
395 chrome
.fileBrowserPrivate
.onPreferencesChanged
.addListener(
397 chrome
.fileBrowserPrivate
.onNetworkConnectionChanged
.addListener(
399 stateChangeHandler();
403 this.initDataTransferOperations_();
405 this.initContextMenus_();
406 this.initCommands_();
408 this.updateFileTypeFilter_();
410 this.selectionHandler_
.onSelectionChanged();
412 this.setupCurrentDirectory_(true /* page loading */);
414 this.table_
.endBatchUpdates();
415 this.grid_
.endBatchUpdates();
417 // Show the page now unless it's already delayed.
420 metrics
.recordInterval('Load.DOM');
421 metrics
.recordInterval('Load.Total');
424 FileManager
.prototype.initDateTimeFormatters_ = function() {
425 var use12hourClock
= !this.preferences_
['use24hourClock'];
426 this.table_
.setDateTimeFormat(use12hourClock
);
429 FileManager
.prototype.initDataTransferOperations_ = function() {
430 this.copyManager_
= new FileCopyManagerWrapper
.getInstance(
431 this.filesystem_
.root
);
433 this.butterBar_
= new ButterBar(this.dialogDom_
, this.copyManager_
,
434 this.metadataCache_
);
435 this.directoryModel_
.addEventListener('directory-changed',
436 this.butterBar_
.forceDeleteAndHide
.bind(this.butterBar_
));
438 // CopyManager and ButterBar are required for 'Delete' operation in
439 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
440 if (this.dialogType
!= DialogType
.FULL_PAGE
) return;
442 this.copyManager_
.addEventListener('copy-progress',
443 this.onCopyProgress_
.bind(this));
444 this.copyManager_
.addEventListener('copy-operation-complete',
445 this.onCopyManagerOperationComplete_
.bind(this));
447 var controller
= this.fileTransferController_
=
448 new FileTransferController(this.document_
,
450 this.directoryModel_
);
451 controller
.attachDragSource(this.table_
.list
);
452 controller
.attachDropTarget(this.table_
.list
);
453 controller
.attachDragSource(this.grid_
);
454 controller
.attachDropTarget(this.grid_
);
455 controller
.attachDropTarget(this.rootsList_
, true);
456 controller
.attachBreadcrumbsDropTarget(this.breadcrumbs_
);
457 controller
.attachCopyPasteHandlers();
458 controller
.addEventListener('selection-copied',
459 this.blinkSelection
.bind(this));
460 controller
.addEventListener('selection-cut',
461 this.blinkSelection
.bind(this));
465 * One-time initialization of context menus.
467 FileManager
.prototype.initContextMenus_ = function() {
468 this.fileContextMenu_
= this.dialogDom_
.querySelector('#file-context-menu');
469 cr
.ui
.Menu
.decorate(this.fileContextMenu_
);
471 cr
.ui
.contextMenuHandler
.setContextMenu(this.grid_
, this.fileContextMenu_
);
472 cr
.ui
.contextMenuHandler
.setContextMenu(this.table_
.querySelector('.list'),
473 this.fileContextMenu_
);
474 cr
.ui
.contextMenuHandler
.setContextMenu(
475 this.document_
.querySelector('.gdrive-welcome.page'),
476 this.fileContextMenu_
);
478 this.rootsContextMenu_
=
479 this.dialogDom_
.querySelector('#roots-context-menu');
480 cr
.ui
.Menu
.decorate(this.rootsContextMenu_
);
482 this.textContextMenu_
=
483 this.dialogDom_
.querySelector('#text-context-menu');
484 cr
.ui
.Menu
.decorate(this.textContextMenu_
);
486 this.gdataSettingsMenu_
= this.dialogDom_
.querySelector('#gdata-settings');
487 cr
.ui
.decorate(this.gdataSettingsMenu_
, cr
.ui
.MenuButton
);
489 this.gdataSettingsMenu_
.addEventListener('menushow',
490 this.onGDataMenuShow_
.bind(this));
492 this.syncButton
.checkable
= true;
493 this.hostedButton
.checkable
= true;
497 * One-time initialization of commands.
499 FileManager
.prototype.initCommands_ = function() {
500 var commandButtons
= this.dialogDom_
.querySelectorAll('button[command]');
501 for (var j
= 0; j
< commandButtons
.length
; j
++)
502 CommandButton
.decorate(commandButtons
[j
]);
504 // TODO(dzvorygin): Here we use this hack, since 'hidden' is standard
505 // attribute and we can't use it's setter as usual.
506 cr
.ui
.Command
.prototype.setHidden = function(value
) {
507 this.__lookupSetter__('hidden').call(this, value
);
510 var commands
= this.dialogDom_
.querySelectorAll('command');
511 for (var i
= 0; i
< commands
.length
; i
++)
512 cr
.ui
.Command
.decorate(commands
[i
]);
514 var doc
= this.document_
;
516 CommandUtil
.registerCommand(doc
, 'newfolder',
517 Commands
.newFolderCommand
, this, this.directoryModel_
);
519 CommandUtil
.registerCommand(this.rootsList_
, 'unmount',
520 Commands
.unmountCommand
, this.rootsList_
, this);
522 CommandUtil
.registerCommand(doc
, 'format',
523 Commands
.formatCommand
, this.rootsList_
, this);
525 CommandUtil
.registerCommand(this.rootsList_
, 'import-photos',
526 Commands
.importCommand
, this.rootsList_
);
528 CommandUtil
.registerCommand(doc
, 'delete',
529 Commands
.deleteFileCommand
, this);
531 CommandUtil
.registerCommand(doc
, 'rename',
532 Commands
.renameFileCommand
, this);
534 CommandUtil
.registerCommand(doc
, 'gdata-buy-more-space',
535 Commands
.gdataBuySpaceCommand
, this);
537 CommandUtil
.registerCommand(doc
, 'gdata-help',
538 Commands
.gdataHelpCommand
, this);
540 CommandUtil
.registerCommand(doc
, 'gdata-clear-local-cache',
541 Commands
.gdataClearCacheCommand
, this);
543 CommandUtil
.registerCommand(doc
, 'gdata-reload',
544 Commands
.gdataReloadCommand
, this);
546 CommandUtil
.registerCommand(doc
, 'gdata-go-to-drive',
547 Commands
.gdataGoToDriveCommand
, this);
549 CommandUtil
.registerCommand(doc
, 'paste',
550 Commands
.pasteFileCommand
, doc
, this.fileTransferController_
);
552 CommandUtil
.registerCommand(doc
, 'open-with',
553 Commands
.openWithCommand
, this);
555 CommandUtil
.registerCommand(doc
, 'toggle-pinned',
556 Commands
.togglePinnedCommand
, this);
558 CommandUtil
.registerCommand(doc
, 'zip-selection',
559 Commands
.zipSelectionCommand
, this);
561 CommandUtil
.registerCommand(doc
, 'search', Commands
.searchCommand
, this,
562 this.dialogDom_
.querySelector('#search-box'));
564 CommandUtil
.registerCommand(doc
, 'cut', Commands
.defaultCommand
, doc
);
565 CommandUtil
.registerCommand(doc
, 'copy', Commands
.defaultCommand
, doc
);
567 var inputs
= this.dialogDom_
.querySelectorAll(
568 'input[type=text], input[type=search], textarea');
570 for (i
= 0; i
< inputs
.length
; i
++) {
571 cr
.ui
.contextMenuHandler
.setContextMenu(inputs
[i
], this.textContextMenu_
);
572 this.registerInputCommands_(inputs
[i
]);
575 cr
.ui
.contextMenuHandler
.setContextMenu(this.renameInput_
,
576 this.textContextMenu_
);
577 this.registerInputCommands_(this.renameInput_
);
579 doc
.addEventListener('command', this.setNoHover_
.bind(this, true));
583 * Registers cut, copy, paste and delete commands on input element.
584 * @param {Node} node Text input element to register on.
586 FileManager
.prototype.registerInputCommands_ = function(node
) {
587 var defaultCommand
= Commands
.defaultCommand
;
588 CommandUtil
.forceDefaultHandler(node
, 'cut');
589 CommandUtil
.forceDefaultHandler(node
, 'copy');
590 CommandUtil
.forceDefaultHandler(node
, 'paste');
591 CommandUtil
.forceDefaultHandler(node
, 'delete');
595 * One-time initialization of dialogs.
597 FileManager
.prototype.initDialogs_ = function() {
598 var d
= cr
.ui
.dialogs
;
599 d
.BaseDialog
.OK_LABEL
= str('OK_LABEL');
600 d
.BaseDialog
.CANCEL_LABEL
= str('CANCEL_LABEL');
601 this.alert
= new d
.AlertDialog(this.dialogDom_
);
602 this.confirm
= new d
.ConfirmDialog(this.dialogDom_
);
603 this.prompt
= new d
.PromptDialog(this.dialogDom_
);
604 this.defaultTaskPicker
=
605 new cr
.filebrowser
.DefaultActionDialog(this.dialogDom_
);
609 * One-time initialization of various DOM nodes.
611 FileManager
.prototype.initDom_ = function() {
612 this.dialogDom_
.addEventListener('drop', function(e
) {
613 // Prevent opening an URL by dropping it onto the page.
617 this.dialogDom_
.addEventListener('click',
618 this.onExternalLinkClick_
.bind(this));
619 // Cache nodes we'll be manipulating.
620 var dom
= this.dialogDom_
;
622 this.filenameInput_
= dom
.querySelector('#filename-input-box input');
623 this.taskItems_
= dom
.querySelector('#tasks');
624 this.okButton_
= dom
.querySelector('.ok');
625 this.cancelButton_
= dom
.querySelector('.cancel');
627 this.table_
= dom
.querySelector('.detail-table');
628 this.grid_
= dom
.querySelector('.thumbnail-grid');
629 this.spinner_
= dom
.querySelector('#spinner-with-text');
630 this.showSpinner_(false);
632 this.breadcrumbs_
= new BreadcrumbsController(
633 dom
.querySelector('#dir-breadcrumbs'));
634 this.breadcrumbs_
.addEventListener(
635 'pathclick', this.onBreadcrumbClick_
.bind(this));
636 this.searchBreadcrumbs_
= new BreadcrumbsController(
637 dom
.querySelector('#search-breadcrumbs'));
638 this.searchBreadcrumbs_
.addEventListener(
639 'pathclick', this.onBreadcrumbClick_
.bind(this));
640 this.searchBreadcrumbs_
.setHideLast(true);
642 var fullPage
= this.dialogType
== DialogType
.FULL_PAGE
;
643 FileTable
.decorate(this.table_
, this.metadataCache_
, fullPage
);
644 FileGrid
.decorate(this.grid_
, this.metadataCache_
);
646 this.document_
.addEventListener('keydown', this.onKeyDown_
.bind(this));
647 this.document_
.addEventListener('keyup', this.onKeyUp_
.bind(this));
649 this.renameInput_
= this.document_
.createElement('input');
650 this.renameInput_
.className
= 'rename';
652 this.renameInput_
.addEventListener(
653 'keydown', this.onRenameInputKeyDown_
.bind(this));
654 this.renameInput_
.addEventListener(
655 'blur', this.onRenameInputBlur_
.bind(this));
657 this.filenameInput_
.addEventListener(
658 'keydown', this.onFilenameInputKeyDown_
.bind(this));
659 this.filenameInput_
.addEventListener(
660 'focus', this.onFilenameInputFocus_
.bind(this));
662 this.listContainer_
= this.dialogDom_
.querySelector('#list-container');
663 this.listContainer_
.addEventListener(
664 'keydown', this.onListKeyDown_
.bind(this));
665 this.listContainer_
.addEventListener(
666 'keypress', this.onListKeyPress_
.bind(this));
667 this.listContainer_
.addEventListener(
668 'mousemove', this.onListMouseMove_
.bind(this));
670 this.okButton_
.addEventListener('click', this.onOk_
.bind(this));
671 this.onCancelBound_
= this.onCancel_
.bind(this);
672 this.cancelButton_
.addEventListener('click', this.onCancelBound_
);
674 this.decorateSplitter(
675 this.dialogDom_
.querySelector('div.sidebar-splitter'));
677 this.dialogContainer_
= this.dialogDom_
.querySelector('.dialog-container');
678 this.dialogDom_
.querySelector('#detail-view').addEventListener(
679 'click', this.onDetailViewButtonClick_
.bind(this));
680 this.dialogDom_
.querySelector('#thumbnail-view').addEventListener(
681 'click', this.onThumbnailViewButtonClick_
.bind(this));
683 this.syncButton
= this.dialogDom_
.querySelector('#gdata-sync-settings');
684 this.syncButton
.addEventListener('activate', this.onGDataPrefClick_
.bind(
685 this, 'cellularDisabled', false /* not inverted */));
687 this.hostedButton
= this.dialogDom_
.querySelector('#gdata-hosted-settings');
688 this.hostedButton
.addEventListener('activate', this.onGDataPrefClick_
.bind(
689 this, 'hostedFilesDisabled', true /* inverted */));
691 cr
.ui
.ComboButton
.decorate(this.taskItems_
);
692 this.taskItems_
.addEventListener('select',
693 this.onTaskItemClicked_
.bind(this));
695 this.dialogDom_
.ownerDocument
.defaultView
.addEventListener(
696 'resize', this.onResize_
.bind(this));
698 if (loadTimeData
.getBoolean('ASH'))
699 this.dialogDom_
.setAttribute('ash', 'true');
701 this.filePopup_
= null;
703 this.dialogDom_
.querySelector('#search-box').addEventListener(
704 'input', this.onSearchBoxUpdate_
.bind(this));
706 this.defaultActionMenuItem_
=
707 this.dialogDom_
.querySelector('#default-action');
709 this.openWithCommand_
=
710 this.dialogDom_
.querySelector('#open-with');
712 this.defaultActionMenuItem_
.addEventListener('activate',
713 this.dispatchSelectionAction_
.bind(this));
715 this.fileTypeSelector_
= this.dialogDom_
.querySelector('#file-type');
716 this.initFileTypeFilter_();
718 this.updateWindowState_();
719 // Populate the static localized strings.
720 i18nTemplate
.process(this.document_
, loadTimeData
);
723 FileManager
.prototype.onBreadcrumbClick_ = function(event
) {
724 this.directoryModel_
.changeDirectory(event
.path
);
728 * Constructs table and grid (heavy operation).
729 * @param {Object} prefs Preferences.
731 FileManager
.prototype.initFileList_ = function(prefs
) {
732 // Always sharing the data model between the detail/thumb views confuses
733 // them. Instead we maintain this bogus data model, and hook it up to the
734 // view that is not in use.
735 this.emptyDataModel_
= new cr
.ui
.ArrayDataModel([]);
736 this.emptySelectionModel_
= new cr
.ui
.ListSelectionModel();
738 var singleSelection
=
739 this.dialogType
== DialogType
.SELECT_OPEN_FILE
||
740 this.dialogType
== DialogType
.SELECT_FOLDER
||
741 this.dialogType
== DialogType
.SELECT_SAVEAS_FILE
;
743 this.directoryModel_
= new DirectoryModel(
744 this.filesystem_
.root
,
748 this.isGDataEnabled());
750 this.directoryModel_
.start();
752 this.selectionHandler_
= new SelectionHandler(this);
754 this.fileWatcher_
= new FileManager
.MetadataFileWatcher(this);
755 this.fileWatcher_
.start();
757 var dataModel
= this.directoryModel_
.getFileList();
759 this.table_
.setupCompareFunctions(dataModel
);
761 dataModel
.addEventListener('permuted',
762 this.updateStartupPrefs_
.bind(this));
764 this.directoryModel_
.getFileListSelection().addEventListener('change',
765 this.selectionHandler_
.onSelectionChanged
.bind(
766 this.selectionHandler_
));
768 this.initList_(this.grid_
);
769 this.initList_(this.table_
.list
);
771 var fileListFocusBound
= this.onFileListFocus_
.bind(this);
773 this.table_
.list
.addEventListener('focus', fileListFocusBound
);
774 this.grid_
.addEventListener('focus', fileListFocusBound
);
776 this.initRootsList_();
778 this.table_
.addEventListener('column-resize-end',
779 this.updateStartupPrefs_
.bind(this));
781 this.setListType(prefs
.listType
|| FileManager
.ListType
.DETAIL
);
784 var cm
= this.table_
.columnModel
;
785 for (var i
= 0; i
< cm
.totalSize
; i
++) {
786 if (prefs
.columns
[i
] > 0)
787 cm
.setWidth(i
, prefs
.columns
[i
]);
791 this.textSearchState_
= {text
: '', date
: new Date()};
793 this.closeOnUnmount_
= (this.params_
.action
== 'auto-open');
795 if (this.closeOnUnmount_
) {
796 this.volumeManager_
.addEventListener('externally-unmounted',
797 this.onExternallyUnmounted_
.bind(this));
799 // Update metadata to change 'Today' and 'Yesterday' dates.
800 var today
= new Date();
804 today
.setMilliseconds(0);
805 setTimeout(this.dailyUpdateModificationTime_
.bind(this),
806 today
.getTime() + MILLISECONDS_IN_DAY
- Date
.now() + 1000);
809 FileManager
.prototype.initRootsList_ = function() {
810 this.rootsList_
= this.dialogDom_
.querySelector('#roots-list');
811 cr
.ui
.List
.decorate(this.rootsList_
);
813 // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox'
814 // role for better accessibility on ChromeOS.
815 this.rootsList_
.setAttribute('role', 'listbox');
818 this.rootsList_
.itemConstructor = function(entry
) {
819 return self
.renderRoot_(entry
.fullPath
);
822 this.rootsList_
.selectionModel
=
823 this.directoryModel_
.getRootsListSelectionModel();
825 // TODO(dgozman): add "Add a drive" item.
826 this.rootsList_
.dataModel
= this.directoryModel_
.getRootsList();
829 FileManager
.prototype.updateStartupPrefs_ = function() {
830 var sortStatus
= this.directoryModel_
.getFileList().sortStatus
;
832 sortField
: sortStatus
.field
,
833 sortDirection
: sortStatus
.direction
,
836 var cm
= this.table_
.columnModel
;
837 for (var i
= 0; i
< cm
.totalSize
; i
++) {
838 prefs
.columns
.push(cm
.getWidth(i
));
840 if (DialogType
.isModal(this.dialogType
))
841 prefs
.listType
= this.listType
;
842 // Save the global default.
843 util
.platform
.setPreference(this.startupPrefName_
, JSON
.stringify(prefs
));
845 // Save the window-specific preference.
846 if (window
.appState
) {
847 window
.appState
.viewOptions
= prefs
;
852 FileManager
.prototype.refocus = function() {
853 if (this.dialogType
== DialogType
.SELECT_SAVEAS_FILE
)
854 this.filenameInput_
.focus();
856 this.currentList_
.focus();
860 * File list focus handler.
862 FileManager
.prototype.onFileListFocus_ = function() {
863 var selection
= this.getSelection();
864 if (!selection
|| selection
.totalCount
!= 0)
867 this.directoryModel_
.selectIndex(0);
871 * Index of selected item in the typeList of the dialog params.
872 * @return {number} 1-based index of selected type or 0 if no type selected.
874 FileManager
.prototype.getSelectedFilterIndex_ = function() {
875 var index
= Number(this.fileTypeSelector_
.selectedIndex
);
876 if (index
< 0) // Nothing selected.
878 if (this.params_
.includeAllFiles
) // Already 1-based.
880 return index
+ 1; // Convert to 1-based;
883 FileManager
.prototype.getRootEntry_ = function(index
) {
887 return this.rootsList_
.dataModel
.item(index
);
890 FileManager
.prototype.setListType = function(type
) {
891 if (type
&& type
== this.listType_
)
894 this.table_
.list
.startBatchUpdates();
895 this.grid_
.startBatchUpdates();
897 // TODO(dzvorygin): style.display and dataModel setting order shouldn't
898 // cause any UI bugs. Currently, the only right way is first to set display
899 // style and only then set dataModel.
901 if (type
== FileManager
.ListType
.DETAIL
) {
902 this.table_
.dataModel
= this.directoryModel_
.getFileList();
903 this.table_
.selectionModel
= this.directoryModel_
.getFileListSelection();
904 this.table_
.style
.display
= '';
905 this.grid_
.style
.display
= 'none';
906 this.grid_
.selectionModel
= this.emptySelectionModel_
;
907 this.grid_
.dataModel
= this.emptyDataModel_
;
908 this.table_
.style
.display
= '';
909 /** @type {cr.ui.List} */
910 this.currentList_
= this.table_
.list
;
911 this.dialogDom_
.querySelector('#detail-view').disabled
= true;
912 this.dialogDom_
.querySelector('#thumbnail-view').disabled
= false;
913 } else if (type
== FileManager
.ListType
.THUMBNAIL
) {
914 this.grid_
.dataModel
= this.directoryModel_
.getFileList();
915 this.grid_
.selectionModel
= this.directoryModel_
.getFileListSelection();
916 this.grid_
.style
.display
= '';
917 this.table_
.style
.display
= 'none';
918 this.table_
.selectionModel
= this.emptySelectionModel_
;
919 this.table_
.dataModel
= this.emptyDataModel_
;
920 this.grid_
.style
.display
= '';
921 /** @type {cr.ui.List} */
922 this.currentList_
= this.grid_
;
923 this.dialogDom_
.querySelector('#thumbnail-view').disabled
= true;
924 this.dialogDom_
.querySelector('#detail-view').disabled
= false;
926 throw new Error('Unknown list type: ' + type
);
929 this.listType_
= type
;
930 this.updateStartupPrefs_();
933 this.table_
.list
.endBatchUpdates();
934 this.grid_
.endBatchUpdates();
938 * Initialize the file list table or grid.
939 * @param {cr.ui.List} list The list.
941 FileManager
.prototype.initList_ = function(list
) {
942 // Overriding the default role 'list' to 'listbox' for better accessibility
944 list
.setAttribute('role', 'listbox');
945 list
.addEventListener('click', this.onDetailClick_
.bind(this));
948 FileManager
.prototype.onCopyProgress_ = function(event
) {
949 if (event
.reason
=== 'ERROR' &&
950 event
.error
.reason
=== 'FILESYSTEM_ERROR' &&
951 event
.error
.data
.toGDrive
&&
952 event
.error
.data
.code
== FileError
.QUOTA_EXCEEDED_ERR
) {
954 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
955 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
957 event
.error
.data
.sourceFileUrl
.split('/').pop()),
958 FileManager
.GOOGLE_DRIVE_BUY_STORAGE
));
961 // TODO(benchan): Currently, there is no FileWatcher emulation for
962 // DriveFileSystem, so we need to manually trigger the directory rescan
963 // after paste operations complete. Remove this once we emulate file
964 // watching functionalities in DriveFileSystem.
965 if (this.isOnGData()) {
966 if (event
.reason
== 'SUCCESS' || event
.reason
== 'ERROR' ||
967 event
.reason
== 'CANCELLED') {
968 this.directoryModel_
.rescanLater();
974 * Handler of file manager operations. Update directory model
975 * to reflect operation result immediatelly (not waiting directory
978 FileManager
.prototype.onCopyManagerOperationComplete_ = function(event
) {
979 var currentPath
= this.directoryModel_
.getCurrentDirPath();
980 if (this.isOnGData() && this.directoryModel_
.isSearching())
983 function inCurrentDirectory(entry
) {
984 var fullPath
= entry
.fullPath
;
985 var dirPath
= fullPath
.substr(0, fullPath
.length
-
986 entry
.name
.length
- 1);
987 return dirPath
== currentPath
;
989 for (var i
= 0; i
< event
.affectedEntries
.length
; i
++) {
990 entry
= event
.affectedEntries
[i
];
991 if (inCurrentDirectory(entry
))
992 this.directoryModel_
.onEntryChanged(entry
.name
);
997 * Fills the file type list or hides it.
999 FileManager
.prototype.initFileTypeFilter_ = function() {
1000 if (this.params_
.includeAllFiles
) {
1001 var option
= this.document_
.createElement('option');
1002 option
.innerText
= str('ALL_FILES_FILTER');
1003 this.fileTypeSelector_
.appendChild(option
);
1007 for (var i
= 0; i
< this.fileTypes_
.length
; i
++) {
1008 var fileType
= this.fileTypes_
[i
];
1009 var option
= this.document_
.createElement('option');
1010 var description
= fileType
.description
;
1012 // See if all the extensions in the group have the same description.
1013 for (var j
= 0; j
!= fileType
.extensions
.length
; j
++) {
1014 var currentDescription
=
1015 FileType
.getTypeString('.' + fileType
.extensions
[j
]);
1016 if (!description
) // Set the first time.
1017 description
= currentDescription
;
1018 else if (description
!= currentDescription
) {
1019 // No single description, fall through to the extension list.
1026 // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1027 description
= fileType
.extensions
.map(function(s
) {
1031 option
.innerText
= description
;
1033 option
.value
= i
+ 1;
1035 if (fileType
.selected
)
1036 option
.selected
= true;
1038 this.fileTypeSelector_
.appendChild(option
);
1041 var options
= this.fileTypeSelector_
.querySelectorAll('option');
1042 if (options
.length
< 2) {
1043 // There is in fact no choice, hide the selector.
1044 this.fileTypeSelector_
.hidden
= true;
1048 this.fileTypeSelector_
.addEventListener('change',
1049 this.updateFileTypeFilter_
.bind(this));
1053 * Filters file according to the selected file type.
1055 FileManager
.prototype.updateFileTypeFilter_ = function() {
1056 this.directoryModel_
.removeFilter('fileType');
1057 var selectedIndex
= this.getSelectedFilterIndex_();
1058 if (selectedIndex
> 0) { // Specific filter selected.
1059 var regexp
= new RegExp('.*(' +
1060 this.fileTypes_
[selectedIndex
- 1].extensions
.join('|') + ')$', 'i');
1061 function filter(entry
) {
1062 return entry
.isDirectory
|| regexp
.test(entry
.name
);
1064 this.directoryModel_
.addFilter('fileType', filter
);
1066 this.directoryModel_
.rescan();
1070 * Respond to the back and forward buttons.
1072 FileManager
.prototype.onPopState_ = function(event
) {
1073 this.closeFilePopup_();
1074 this.setupCurrentDirectory_(false /* page loading */);
1077 FileManager
.prototype.requestResize_ = function(timeout
) {
1078 setTimeout(this.onResize_
.bind(this), timeout
|| 0);
1082 * Resize details and thumb views to fit the new window size.
1084 FileManager
.prototype.onResize_ = function() {
1085 if (this.listType_
== FileManager
.ListType
.THUMBNAIL
) {
1087 g
.startBatchUpdates();
1088 setTimeout(function() {
1091 g
.endBatchUpdates();
1094 this.table_
.redraw();
1097 this.rootsList_
.redraw();
1098 this.breadcrumbs_
.truncate();
1099 this.searchBreadcrumbs_
.truncate();
1101 this.updateWindowState_();
1104 FileManager
.prototype.updateWindowState_ = function() {
1105 util
.platform
.getWindowStatus(function(wnd
) {
1106 if (wnd
.state
== 'maximized') {
1107 this.dialogDom_
.setAttribute('maximized', 'maximized');
1109 this.dialogDom_
.removeAttribute('maximized');
1114 FileManager
.prototype.resolvePath = function(
1115 path
, resultCallback
, errorCallback
) {
1116 return util
.resolvePath(this.filesystem_
.root
, path
, resultCallback
,
1121 * Restores current directory and may be a selected item after page load (or
1122 * reload) or popping a state (after click on back/forward). If location.hash
1123 * is present it means that the user has navigated somewhere and that place
1124 * will be restored. defaultPath primarily is used with save/open dialogs.
1125 * Default path may also contain a file name. Freshly opened file manager
1126 * window has neither.
1128 * @param {boolean} pageLoading True if the page is loading,
1129 false if popping state.
1131 FileManager
.prototype.setupCurrentDirectory_ = function(pageLoading
) {
1132 var path
= location
.hash
? // Location hash has the highest priority.
1133 decodeURI(location
.hash
.substr(1)) :
1136 if (!pageLoading
&& path
== this.directoryModel_
.getCurrentDirPath())
1140 path
= this.directoryModel_
.getDefaultDirectory();
1141 } else if (path
.indexOf('/') == -1) {
1142 // Path is a file name.
1143 path
= this.directoryModel_
.getDefaultDirectory() + '/' + path
;
1146 // In the FULL_PAGE mode if the hash path points to a file we might have
1147 // to invoke a task after selecting it.
1148 // If the file path is in params_ we only want to select the file.
1149 var invokeHandlers
= pageLoading
&& (this.params_
.action
!= 'select') &&
1150 this.dialogType
== DialogType
.FULL_PAGE
;
1152 if (PathUtil
.getRootType(path
) === RootType
.GDATA
) {
1153 var tracker
= this.directoryModel_
.createDirectoryChangeTracker();
1154 // Expected finish of setupPath to GData.
1155 tracker
.exceptInitialChange
= true;
1157 if (!this.isGDataEnabled()) {
1160 var leafName
= path
.substr(path
.indexOf('/') + 1);
1161 path
= this.directoryModel_
.getDefaultDirectory() + '/' + leafName
;
1162 this.finishSetupCurrentDirectory_(path
, invokeHandlers
);
1165 var gdataPath
= RootDirectory
.GDATA
;
1166 if (this.volumeManager_
.isMounted(gdataPath
)) {
1167 this.finishSetupCurrentDirectory_(path
, invokeHandlers
);
1171 this.delayShow_(500);
1172 // Reflect immediatelly in the UI we are on GData and display
1174 this.directoryModel_
.setupPath(gdataPath
);
1176 if (!this.isOnGData()) {
1177 // Since GDATA is not mounted it should be resolved synchronously
1178 // (no need in asynchronous calls to filesystem API). It is important
1179 // to prevent race condition.
1180 console
.error('Expected path set up synchronously');
1184 this.volumeManager_
.mountGData(function() {
1186 if (!tracker
.hasChanged
) {
1187 self
.finishSetupCurrentDirectory_(path
, invokeHandlers
);
1189 }, function(error
) {
1193 if (invokeHandlers
&& pageLoading
)
1194 this.delayShow_(500);
1195 this.finishSetupCurrentDirectory_(path
, invokeHandlers
);
1200 * @param {string} path Path to setup.
1201 * @param {boolean} invokeHandlers If thrue and |path| points to a file
1202 * then default handler is triggered.
1204 FileManager
.prototype.finishSetupCurrentDirectory_ = function(
1205 path
, invokeHandlers
) {
1206 if (invokeHandlers
) {
1207 var onResolve = function(baseName
, leafName
, exists
) {
1211 if (!exists
|| leafName
== '') {
1212 // Non-existent file or a directory.
1213 if (this.params_
.gallery
) {
1214 // Reloading while the Gallery is open with empty or multiple
1215 // selection. Open the Gallery when the directory is scanned.
1220 // There are 3 ways we can get here:
1221 // 1. Invoked from file_manager_util::ViewFile. This can only
1222 // happen for 'gallery' and 'mount-archive' actions.
1223 // 2. Reloading a Gallery page. Must be an image or a video file.
1224 // 3. A user manually entered a URL pointing to a file.
1225 // We call the appropriate methods of FileTasks directly as we do
1226 // not need any of the preparations that |execute| method does.
1227 if (FileType
.isImageOrVideo(path
)) {
1228 urls
= [util
.makeFilesystemUrl(path
)];
1231 if (FileType
.getMediaType(path
) == 'archive') {
1232 urls
= [util
.makeFilesystemUrl(path
)];
1233 action
= 'archives';
1238 var listener = function() {
1239 this.directoryModel_
.removeEventListener(
1240 'scan-completed', listener
);
1241 var tasks
= new FileTasks(this, this.params_
);
1242 if (action
== 'gallery') {
1243 tasks
.openGallery(urls
);
1244 } else if (action
== 'archives') {
1245 tasks
.mountArchives_(urls
);
1248 this.directoryModel_
.addEventListener('scan-completed', listener
);
1251 if (action
!= 'gallery') {
1252 // Opening gallery will invoke |this.show_| at the right time.
1257 this.directoryModel_
.setupPath(path
, onResolve
);
1261 if (this.dialogType
== DialogType
.SELECT_SAVEAS_FILE
) {
1262 this.directoryModel_
.setupPath(path
, function(basePath
, leafName
) {
1263 this.filenameInput_
.value
= leafName
;
1264 this.selectDefaultPathInFilenameInput_();
1270 this.directoryModel_
.setupPath(path
);
1274 * Tweak the UI to become a particular kind of dialog, as determined by the
1275 * dialog type parameter passed to the constructor.
1277 FileManager
.prototype.initDialogType_ = function() {
1279 var okLabel
= str('OPEN_LABEL');
1281 switch (this.dialogType
) {
1282 case DialogType
.SELECT_FOLDER
:
1283 defaultTitle
= str('SELECT_FOLDER_TITLE');
1286 case DialogType
.SELECT_OPEN_FILE
:
1287 defaultTitle
= str('SELECT_OPEN_FILE_TITLE');
1290 case DialogType
.SELECT_OPEN_MULTI_FILE
:
1291 defaultTitle
= str('SELECT_OPEN_MULTI_FILE_TITLE');
1294 case DialogType
.SELECT_SAVEAS_FILE
:
1295 defaultTitle
= str('SELECT_SAVEAS_FILE_TITLE');
1296 okLabel
= str('SAVE_LABEL');
1299 case DialogType
.FULL_PAGE
:
1303 throw new Error('Unknown dialog type: ' + this.dialogType
);
1306 this.okButton_
.textContent
= okLabel
;
1308 var dialogTitle
= this.params_
.title
|| defaultTitle
;
1309 this.dialogDom_
.querySelector('.dialog-title').textContent
= dialogTitle
;
1311 this.dialogDom_
.setAttribute('type', this.dialogType
);
1314 FileManager
.prototype.renderRoot_ = function(path
) {
1315 var li
= this.document_
.createElement('li');
1316 li
.className
= 'root-item';
1317 li
.setAttribute('role', 'option');
1318 var dm
= this.directoryModel_
;
1319 var handleClick = function() {
1320 if (li
.selected
&& path
!== dm
.getCurrentDirPath()) {
1321 dm
.changeDirectory(path
);
1324 li
.addEventListener('mousedown', handleClick
);
1325 li
.addEventListener(cr
.ui
.TouchHandler
.EventType
.TOUCH_START
, handleClick
);
1327 var rootType
= PathUtil
.getRootType(path
);
1329 var div
= this.document_
.createElement('div');
1330 div
.className
= 'root-label';
1332 div
.setAttribute('volume-type-icon', rootType
);
1333 if (rootType
=== RootType
.REMOVABLE
)
1334 div
.setAttribute('volume-subtype',
1335 this.volumeManager_
.getDeviceType(path
));
1337 div
.textContent
= PathUtil
.getRootLabel(path
);
1338 li
.appendChild(div
);
1340 if (rootType
=== RootType
.ARCHIVE
|| rootType
=== RootType
.REMOVABLE
) {
1341 var eject
= this.document_
.createElement('div');
1342 eject
.className
= 'root-eject';
1343 eject
.addEventListener('click', function(event
) {
1344 event
.stopPropagation();
1345 var unmountCommand
= this.dialogDom_
.querySelector('command#unmount');
1346 // Let's make sure 'canExecute' state of the command is properly set for
1347 // the root before executing it.
1348 unmountCommand
.canExecuteChange(li
);
1349 unmountCommand
.execute(li
);
1351 // Block other mouse handlers.
1352 eject
.addEventListener('mouseup', function(e
) { e
.stopPropagation() });
1353 eject
.addEventListener('mousedown', function(e
) { e
.stopPropagation() });
1354 li
.appendChild(eject
);
1357 // To enable photo import dialog, set this context menu for Downloads and
1358 // remove 'hidden' attribute on import-photos command.
1359 if (rootType
!= RootType
.GDATA
&& rootType
!= RootType
.DOWNLOADS
) {
1360 cr
.ui
.contextMenuHandler
.setContextMenu(li
, this.rootsContextMenu_
);
1363 cr
.defineProperty(li
, 'lead', cr
.PropertyKind
.BOOL_ATTR
);
1364 cr
.defineProperty(li
, 'selected', cr
.PropertyKind
.BOOL_ATTR
);
1371 * @param {string} path Path to a volume to unmount.
1373 FileManager
.prototype.unmountVolume = function(path
) {
1374 var listItem
= this.rootsList_
.getListItemByIndex(
1375 this.directoryModel_
.findRootsListIndex(path
));
1377 listItem
.setAttribute('disabled', '');
1378 var onError = function(error
) {
1380 listItem
.removeAttribute('disabled');
1381 this.alert
.showHtml('', str('UNMOUNT_FAILED'));
1383 this.volumeManager_
.unmount(path
, function() {}, onError
.bind(this));
1386 FileManager
.prototype.refreshCurrentDirectoryMetadata_ = function() {
1387 var entries
= this.directoryModel_
.getFileList().slice();
1388 // We don't pass callback here. When new metadata arrives, we have an
1389 // observer registered to update the UI.
1391 // TODO(dgozman): refresh content metadata only when modificationTime
1393 this.metadataCache_
.clear(entries
, 'filesystem|thumbnail|media');
1394 this.metadataCache_
.get(entries
, 'filesystem', null);
1395 if (this.isOnGData()) {
1396 this.metadataCache_
.clear(entries
, 'gdata');
1397 this.metadataCache_
.get(entries
, 'gdata', null);
1400 var visibleItems
= this.currentList_
.items
;
1401 var visibleEntries
= [];
1402 for (var i
= 0; i
< visibleItems
.length
; i
++) {
1403 var index
= this.currentList_
.getIndexOfListItem(visibleItems
[i
]);
1404 var entry
= this.directoryModel_
.getFileList().item(index
);
1405 // The following check is a workaround for the bug in list: sometimes item
1406 // does not have listIndex, and therefore is not found in the list.
1407 if (entry
) visibleEntries
.push(entry
);
1409 this.metadataCache_
.get(visibleEntries
, 'thumbnail', null);
1412 FileManager
.prototype.dailyUpdateModificationTime_ = function() {
1413 var fileList
= this.directoryModel_
.getFileList();
1415 for (var i
= 0; i
< fileList
.length
; i
++) {
1416 urls
.push(fileList
.item(i
).toURL());
1418 this.metadataCache_
.get(
1419 fileList
.slice(), 'filesystem',
1420 this.updateMetadataInUI_
.bind(this, 'filesystem', urls
));
1422 setTimeout(this.dailyUpdateModificationTime_
.bind(this),
1423 MILLISECONDS_IN_DAY
);
1426 FileManager
.prototype.updateMetadataInUI_ = function(
1427 type
, urls
, properties
) {
1428 var propertyByUrl
= urls
.reduce(function(map
, url
, index
) {
1429 map
[url
] = properties
[index
];
1433 if (this.listType_
== FileManager
.ListType
.DETAIL
)
1434 this.table_
.updateListItemsMetadata(type
, propertyByUrl
);
1436 this.grid_
.updateListItemsMetadata(type
, propertyByUrl
);
1437 // TODO: update bottom panel thumbnails.
1441 * Restore the item which is being renamed while refreshing the file list. Do
1442 * nothing if no item is being renamed or such an item disappeared.
1444 * While refreshing file list it gets repopulated with new file entries.
1445 * There is not a big difference whether DOM items stay the same or not.
1446 * Except for the item that the user is renaming.
1448 FileManager
.prototype.restoreItemBeingRenamed_ = function() {
1449 if (!this.isRenamingInProgress())
1452 var dm
= this.directoryModel_
;
1453 var leadIndex
= dm
.getFileListSelection().leadIndex
;
1457 var leadEntry
= dm
.getFileList().item(leadIndex
);
1458 if (this.renameInput_
.currentEntry
.fullPath
!= leadEntry
.fullPath
)
1461 var leadListItem
= this.findListItemForNode_(this.renameInput_
);
1462 if (this.currentList_
== this.table_
.list
) {
1463 this.table_
.updateFileMetadata(leadListItem
, leadEntry
);
1465 this.currentList_
.restoreLeadItem(leadListItem
);
1468 FileManager
.prototype.isOnGData = function() {
1469 return this.directoryModel_
.getCurrentRootType() === RootType
.GDATA
;
1473 * Overrides default handling for clicks on hyperlinks.
1474 * Opens them in a separate tab and if it's an open/save dialog
1476 * @param {Event} event Click event.
1478 FileManager
.prototype.onExternalLinkClick_ = function(event
) {
1479 if (event
.target
.tagName
!= 'A' || !event
.target
.href
)
1482 // In a packaged apps links with targer='_blank' open in a new tab by
1483 // default, other links do not open at all.
1484 if (!util
.platform
.v2()) {
1485 chrome
.tabs
.create({url
: event
.target
.href
});
1486 event
.preventDefault();
1489 if (this.dialogType
!= DialogType
.FULL_PAGE
) {
1495 * Task combobox handler.
1497 * @param {Object} event Event containing task which was clicked.
1499 FileManager
.prototype.onTaskItemClicked_ = function(event
) {
1500 var selection
= this.getSelection();
1501 if (!selection
.tasks
) return;
1503 if (event
.item
.task
) {
1504 // Task field doesn't exist on change-default dropdown item.
1505 selection
.tasks
.execute(event
.item
.task
.taskId
);
1507 var extensions
= [];
1509 for (var i
= 0; i
< selection
.urls
.length
; i
++) {
1510 var match
= /\.(\w+)$/g.exec(selection
.urls
[i
]);
1512 var ext
= match
[1].toUpperCase();
1513 if (extensions
.indexOf(ext
) == -1) {
1514 extensions
.push(ext
);
1521 if (extensions
.length
== 1) {
1522 format
= extensions
[0];
1525 // Change default was clicked. We should open "change default" dialog.
1526 selection
.tasks
.showTaskPicker(this.defaultTaskPicker
,
1527 loadTimeData
.getString('CHANGE_DEFAULT_MENU_ITEM'),
1528 strf('CHANGE_DEFAULT_CAPTION', format
),
1529 this.onDefaultTaskDone_
.bind(this));
1535 * Sets the given task as default, when this task is applicable.
1536 * @param {Object} task Task to set as default.
1538 FileManager
.prototype.onDefaultTaskDone_ = function(task
) {
1539 // TODO(dgozman): move this method closer to tasks.
1540 var selection
= this.getSelection();
1541 chrome
.fileBrowserPrivate
.setDefaultTask(task
.taskId
,
1542 selection
.urls
, selection
.mimeTypes
);
1543 selection
.tasks
= new FileTasks(this);
1544 selection
.tasks
.init(selection
.urls
, selection
.mimeTypes
);
1545 selection
.tasks
.display(this.taskItems_
);
1546 this.refreshCurrentDirectoryMetadata_();
1547 this.selectionHandler_
.onSelectionChanged();
1550 FileManager
.prototype.updateNetworkStateAndPreferences_ = function(
1555 if (--downcount
== 0)
1559 chrome
.fileBrowserPrivate
.getPreferences(function(prefs
) {
1560 self
.preferences_
= prefs
;
1564 chrome
.fileBrowserPrivate
.getNetworkConnectionState(function(networkState
) {
1565 self
.networkState_
= networkState
;
1570 FileManager
.prototype.onNetworkStateOrPreferencesChanged_ = function() {
1572 this.updateNetworkStateAndPreferences_(function() {
1573 var gdata
= self
.preferences_
;
1574 var network
= self
.networkState_
;
1576 self
.initDateTimeFormatters_();
1577 self
.refreshCurrentDirectoryMetadata_();
1579 self
.directoryModel_
.setGDataEnabled(self
.isGDataEnabled());
1580 self
.directoryModel_
.setOffline(!network
.online
);
1582 if (gdata
.cellularDisabled
)
1583 self
.syncButton
.setAttribute('checked', '');
1585 self
.syncButton
.removeAttribute('checked');
1587 if (self
.hostedButton
.hasAttribute('checked') !=
1588 gdata
.hostedFilesDisabled
&& self
.isOnGData()) {
1589 self
.directoryModel_
.rescan();
1592 if (!gdata
.hostedFilesDisabled
)
1593 self
.hostedButton
.setAttribute('checked', '');
1595 self
.hostedButton
.removeAttribute('checked');
1597 if (network
.online
) {
1598 if (gdata
.cellularDisabled
&& network
.type
== 'cellular')
1599 self
.dialogContainer_
.setAttribute('connection', 'metered');
1601 self
.dialogContainer_
.removeAttribute('connection');
1603 self
.dialogContainer_
.setAttribute('connection', 'offline');
1608 FileManager
.prototype.isOnMeteredConnection = function() {
1609 return this.preferences_
.cellularDisabled
&&
1610 this.networkState_
.online
&&
1611 this.networkState_
.type
== 'cellular';
1614 FileManager
.prototype.isOffline = function() {
1615 return !this.networkState_
.online
;
1618 FileManager
.prototype.isGDataEnabled = function() {
1619 return !this.params_
.disableGData
&&
1620 (!('driveEnabled' in this.preferences_
) ||
1621 this.preferences_
.driveEnabled
);
1624 FileManager
.prototype.isOnReadonlyDirectory = function() {
1625 return this.directoryModel_
.isReadOnly();
1628 FileManager
.prototype.onExternallyUnmounted_ = function(event
) {
1629 if (event
.mountPath
== this.directoryModel_
.getCurrentRootPath()) {
1630 if (this.closeOnUnmount_
) {
1631 // If the file manager opened automatically when a usb drive inserted,
1632 // user have never changed current volume (that implies the current
1633 // directory is still on the device) then close this tab.
1634 util
.platform
.closeWindow();
1640 * Show a modal-like file viewer/editor on top of the File Manager UI.
1642 * @param {HTMLElement} popup Popup element.
1643 * @param {function} closeCallback Function to call after the popup is closed.
1645 FileManager
.prototype.openFilePopup_ = function(popup
, closeCallback
) {
1646 this.closeFilePopup_();
1647 this.filePopup_
= popup
;
1648 this.filePopupCloseCallback_
= closeCallback
;
1649 this.dialogDom_
.appendChild(this.filePopup_
);
1650 this.filePopup_
.focus();
1651 this.document_
.body
.setAttribute('overlay-visible', '');
1654 FileManager
.prototype.closeFilePopup_ = function() {
1655 if (this.filePopup_
) {
1656 this.document_
.body
.removeAttribute('overlay-visible');
1657 // The window resize would not be processed properly while the relevant
1658 // divs had 'display:none', force resize after the layout fired.
1659 setTimeout(this.onResize_
.bind(this), 0);
1660 if (this.filePopup_
.contentWindow
&&
1661 this.filePopup_
.contentWindow
.unload
) {
1662 this.filePopup_
.contentWindow
.unload();
1665 this.dialogDom_
.removeChild(this.filePopup_
);
1666 this.filePopup_
= null;
1667 if (this.filePopupCloseCallback_
) {
1668 this.filePopupCloseCallback_();
1669 this.filePopupCloseCallback_
= null;
1675 FileManager
.prototype.getAllUrlsInCurrentDirectory = function() {
1677 var fileList
= this.directoryModel_
.getFileList();
1678 for (var i
= 0; i
!= fileList
.length
; i
++) {
1679 urls
.push(fileList
.item(i
).toURL());
1684 FileManager
.prototype.isRenamingInProgress = function() {
1685 return !!this.renameInput_
.currentEntry
;
1688 FileManager
.prototype.focusCurrentList_ = function() {
1689 if (this.listType_
== FileManager
.ListType
.DETAIL
)
1690 this.table_
.focus();
1691 else // this.listType_ == FileManager.ListType.THUMBNAIL)
1696 * Return full path of the current directory or null.
1698 FileManager
.prototype.getCurrentDirectory = function() {
1699 return this.directoryModel_
&&
1700 this.directoryModel_
.getCurrentDirPath();
1704 * Return URL of the current directory or null.
1706 FileManager
.prototype.getCurrentDirectoryURL = function() {
1707 return this.directoryModel_
&&
1708 this.directoryModel_
.getCurrentDirEntry().toURL();
1711 FileManager
.prototype.deleteSelection = function() {
1712 this.butterBar_
.initiateDelete(this.getSelection().entries
);
1715 FileManager
.prototype.blinkSelection = function() {
1716 var selection
= this.getSelection();
1717 if (!selection
|| selection
.totalCount
== 0)
1720 for (var i
= 0; i
< selection
.entries
.length
; i
++) {
1721 var selectedIndex
= selection
.indexes
[i
];
1722 var listItem
= this.currentList_
.getListItemByIndex(selectedIndex
);
1724 this.blinkListItem_(listItem
);
1728 FileManager
.prototype.blinkListItem_ = function(listItem
) {
1729 listItem
.classList
.add('blink');
1730 setTimeout(function() {
1731 listItem
.classList
.remove('blink');
1735 FileManager
.prototype.selectDefaultPathInFilenameInput_ = function() {
1736 var input
= this.filenameInput_
;
1738 var selectionEnd
= input
.value
.lastIndexOf('.');
1739 if (selectionEnd
== -1) {
1742 input
.selectionStart
= 0;
1743 input
.selectionEnd
= selectionEnd
;
1745 // Clear, so we never do this again.
1746 this.defaultPath
= '';
1750 * Handles mouse click or tap.
1751 * @param {Event} event The click event.
1753 FileManager
.prototype.onDetailClick_ = function(event
) {
1754 if (this.isRenamingInProgress()) {
1755 // Don't pay attention to clicks during a rename.
1759 var listItem
= this.findListItemForEvent_(event
);
1760 var selection
= this.getSelection();
1761 if (!listItem
|| !listItem
.selected
|| selection
.totalCount
!= 1) {
1766 if (this.dialogType
== DialogType
.FULL_PAGE
&&
1767 (event
.target
.parentElement
.classList
.contains('filename-label') ||
1768 event
.target
.classList
.contains('detail-icon'))) {
1769 // If full page mode the file name and icon should react on single click.
1771 } else if (this.lastClickedItem_
== listItem
) {
1772 // React on double click, but only if both clicks hit the same item.
1775 this.lastClickedItem_
= listItem
;
1777 if (event
.detail
!= clickNumber
)
1780 var entry
= selection
.entries
[0];
1781 if (entry
.isDirectory
) {
1782 this.onDirectoryAction(entry
);
1784 this.dispatchSelectionAction_();
1788 FileManager
.prototype.dispatchSelectionAction_ = function() {
1789 if (this.dialogType
== DialogType
.FULL_PAGE
) {
1790 var tasks
= this.getSelection().tasks
;
1791 if (tasks
) tasks
.executeDefault();
1794 if (!this.okButton_
.disabled
) {
1802 * Executes directory action (i.e. changes directory).
1804 * @param {DirectoryEntry} entry Directory entry to which directory should be
1807 FileManager
.prototype.onDirectoryAction = function(entry
) {
1808 var mountError
= this.volumeManager_
.getMountError(
1809 PathUtil
.getRootPath(entry
.fullPath
));
1810 if (mountError
== VolumeManager
.Error
.UNKNOWN_FILESYSTEM
) {
1811 return this.butterBar_
.show(str('UNKNOWN_FILESYSTEM_WARNING'));
1812 } else if (mountError
== VolumeManager
.Error
.UNSUPPORTED_FILESYSTEM
) {
1813 return this.butterBar_
.show(str('UNSUPPORTED_FILESYSTEM_WARNING'));
1816 return this.directoryModel_
.changeDirectory(entry
.fullPath
);
1820 * Update the tab title.
1822 FileManager
.prototype.updateTitle_ = function() {
1823 if (this.dialogType
!= DialogType
.FULL_PAGE
)
1826 var path
= this.getCurrentDirectory();
1827 var rootPath
= PathUtil
.getRootPath(path
);
1828 this.document_
.title
= PathUtil
.getRootLabel(rootPath
) +
1829 path
.substring(rootPath
.length
);
1833 * Updates search box value when directory gets changed.
1835 FileManager
.prototype.updateSearchBoxOnDirChange_ = function() {
1836 var searchBox
= this.dialogDom_
.querySelector('#search-box');
1837 if (!searchBox
.disabled
)
1838 searchBox
.value
= '';
1842 * Update the UI when the current directory changes.
1844 * @param {cr.Event} event The directory-changed event.
1846 FileManager
.prototype.onDirectoryChanged_ = function(event
) {
1847 this.selectionHandler_
.onSelectionChanged();
1848 this.updateSearchBoxOnDirChange_();
1849 if (this.dialogType
== DialogType
.FULL_PAGE
)
1850 this.table_
.showOfflineColumn(this.isOnGData());
1852 util
.updateAppState(event
.initial
, this.getCurrentDirectory());
1854 if (this.closeOnUnmount_
&& !event
.initial
&&
1855 PathUtil
.getRootPath(event
.previousDirEntry
.fullPath
) !=
1856 PathUtil
.getRootPath(event
.newDirEntry
.fullPath
)) {
1857 this.closeOnUnmount_
= false;
1860 this.updateUnformattedDriveStatus_();
1862 this.updateTitle_();
1865 FileManager
.prototype.updateUnformattedDriveStatus_ = function() {
1866 var volumeInfo
= this.volumeManager_
.getVolumeInfo_(
1867 this.directoryModel_
.getCurrentRootPath());
1869 if (volumeInfo
.error
) {
1870 this.dialogContainer_
.setAttribute('unformatted', '');
1872 var errorNode
= this.dialogDom_
.querySelector('#format-panel > .error');
1873 if (volumeInfo
.error
== VolumeManager
.Error
.UNSUPPORTED_FILESYSTEM
) {
1874 errorNode
.textContent
= str('UNSUPPORTED_FILESYSTEM_WARNING');
1876 errorNode
.textContent
= str('UNKNOWN_FILESYSTEM_WARNING');
1879 this.dialogContainer_
.removeAttribute('unformatted');
1883 FileManager
.prototype.findListItemForEvent_ = function(event
) {
1884 return this.findListItemForNode_(event
.touchedElement
|| event
.srcElement
);
1887 FileManager
.prototype.findListItemForNode_ = function(node
) {
1888 var item
= this.currentList_
.getListItemAncestor(node
);
1889 // TODO(serya): list should check that.
1890 return item
&& this.currentList_
.isItem(item
) ? item
: null;
1894 * Unload handler for the page. May be called manually for the file picker
1895 * dialog, because it closes by calling extension API functions that do not
1898 FileManager
.prototype.onUnload_ = function() {
1899 this.fileWatcher_
.stop();
1900 if (this.filePopup_
&&
1901 this.filePopup_
.contentWindow
&&
1902 this.filePopup_
.contentWindow
.unload
) {
1903 this.filePopup_
.contentWindow
.unload(true /* exiting */);
1907 FileManager
.prototype.initiateRename = function() {
1908 var item
= this.currentList_
.ensureLeadItemExists();
1911 var label
= item
.querySelector('.filename-label');
1912 var input
= this.renameInput_
;
1914 input
.value
= label
.textContent
;
1915 label
.parentNode
.setAttribute('renaming', '');
1916 label
.parentNode
.appendChild(input
);
1918 var selectionEnd
= input
.value
.lastIndexOf('.');
1919 if (selectionEnd
== -1) {
1922 input
.selectionStart
= 0;
1923 input
.selectionEnd
= selectionEnd
;
1926 // This has to be set late in the process so we don't handle spurious
1928 input
.currentEntry
= this.currentList_
.dataModel
.item(item
.listIndex
);
1931 FileManager
.prototype.onRenameInputKeyDown_ = function(event
) {
1932 if (!this.isRenamingInProgress())
1935 // Do not move selection or lead item in list during rename.
1936 if (event
.keyIdentifier
== 'Up' || event
.keyIdentifier
== 'Down') {
1937 event
.stopPropagation();
1940 switch (util
.getKeyModifiers(event
) + event
.keyCode
) {
1941 case '27': // Escape
1942 this.cancelRename_();
1943 event
.preventDefault();
1947 this.commitRename_();
1948 event
.preventDefault();
1953 FileManager
.prototype.onRenameInputBlur_ = function(event
) {
1954 if (this.isRenamingInProgress() && !this.renameInput_
.validation_
)
1955 this.commitRename_();
1958 FileManager
.prototype.commitRename_ = function() {
1959 var input
= this.renameInput_
;
1960 var entry
= input
.currentEntry
;
1961 var newName
= input
.value
;
1963 if (newName
== entry
.name
) {
1964 this.cancelRename_();
1968 var nameNode
= this.findListItemForNode_(this.renameInput_
).
1969 querySelector('.filename-label');
1971 input
.validation_
= true;
1972 function validationDone() {
1973 input
.validation_
= false;
1974 // Alert dialog restores focus unless the item removed from DOM.
1975 if (this.document_
.activeElement
!= input
)
1976 this.cancelRename_();
1979 if (!this.validateFileName_(newName
, validationDone
.bind(this)))
1982 function onError(err
) {
1983 this.alert
.show(strf('ERROR_RENAMING', entry
.name
,
1984 util
.getFileErrorString(err
.code
)));
1987 this.cancelRename_();
1988 // Optimistically apply new name immediately to avoid flickering in
1990 nameNode
.textContent
= newName
;
1992 this.directoryModel_
.doesExist(entry
, newName
, function(exists
, isFile
) {
1994 this.directoryModel_
.renameEntry(entry
, newName
, onError
.bind(this));
1996 nameNode
.textContent
= entry
.name
;
1997 var message
= isFile
? 'FILE_ALREADY_EXISTS' :
1998 'DIRECTORY_ALREADY_EXISTS';
1999 this.alert
.show(strf(message
, newName
));
2004 FileManager
.prototype.cancelRename_ = function() {
2005 this.renameInput_
.currentEntry
= null;
2007 var parent
= this.renameInput_
.parentNode
;
2009 parent
.removeAttribute('renaming');
2010 parent
.removeChild(this.renameInput_
);
2015 FileManager
.prototype.onFilenameInputKeyDown_ = function(event
) {
2016 var enabled
= this.selectionHandler_
.updateOkButton();
2018 (util
.getKeyModifiers(event
) + event
.keyCode
) == '13' /* Enter */)
2022 FileManager
.prototype.onFilenameInputFocus_ = function(event
) {
2023 var input
= this.filenameInput_
;
2025 // On focus we want to select everything but the extension, but
2026 // Chrome will select-all after the focus event completes. We
2027 // schedule a timeout to alter the focus after that happens.
2028 setTimeout(function() {
2029 var selectionEnd
= input
.value
.lastIndexOf('.');
2030 if (selectionEnd
== -1) {
2033 input
.selectionStart
= 0;
2034 input
.selectionEnd
= selectionEnd
;
2039 FileManager
.prototype.onScanStarted_ = function() {
2040 this.breadcrumbs_
.update(
2041 this.directoryModel_
.getCurrentRootPath(),
2042 this.directoryModel_
.getCurrentDirPath());
2044 this.cancelSpinnerTimeout_();
2045 this.showSpinner_(false);
2046 this.showSpinnerTimeout_
=
2047 setTimeout(this.showSpinner_
.bind(this, true), 500);
2050 FileManager
.prototype.cancelSpinnerTimeout_ = function() {
2051 if (this.showSpinnerTimeout_
) {
2052 clearTimeout(this.showSpinnerTimeout_
);
2053 this.showSpinnerTimeout_
= null;
2057 FileManager
.prototype.hideSpinnerLater_ = function() {
2058 this.cancelSpinnerTimeout_();
2059 this.showSpinnerTimeout_
=
2060 setTimeout(this.showSpinner_
.bind(this, false), 100);
2063 FileManager
.prototype.showSpinner_ = function(on
) {
2064 if (on
&& this.directoryModel_
&& this.directoryModel_
.isScanning()) {
2065 if (this.directoryModel_
.isSearching()) {
2066 this.dialogContainer_
.classList
.add('searching');
2067 this.spinner_
.style
.display
= 'none';
2069 this.spinner_
.style
.display
= '';
2070 this.dialogContainer_
.classList
.remove('searching');
2074 if (!on
&& (!this.directoryModel_
|| !this.directoryModel_
.isScanning())) {
2075 this.spinner_
.style
.display
= 'none';
2076 if (this.dialogContainer_
)
2077 this.dialogContainer_
.classList
.remove('searching');
2081 FileManager
.prototype.createNewFolder = function() {
2082 var defaultName
= str('DEFAULT_NEW_FOLDER_NAME');
2084 // Find a name that doesn't exist in the data model.
2085 var files
= this.directoryModel_
.getFileList();
2087 for (var i
= 0; i
< files
.length
; i
++) {
2088 var name
= files
.item(i
).name
;
2089 // Filtering names prevents from conflicts with prototype's names
2091 if (name
.substring(0, defaultName
.length
) == defaultName
)
2095 var baseName
= defaultName
;
2100 function advance() {
2106 function current() {
2107 return baseName
+ separator
+ index
+ suffix
;
2110 // Accessing hasOwnProperty is safe since hash properties filtered.
2111 while (hash
.hasOwnProperty(current())) {
2116 var list
= self
.currentList_
;
2117 function tryCreate() {
2118 self
.directoryModel_
.createDirectory(current(),
2119 onSuccess
, onError
);
2122 function onSuccess(entry
) {
2123 metrics
.recordUserAction('CreateNewFolder');
2124 list
.selectedItem
= entry
;
2125 self
.initiateRename();
2128 function onError(error
) {
2129 self
.alert
.show(strf('ERROR_CREATING_FOLDER', current(),
2130 util
.getFileErrorString(error
.code
)));
2136 FileManager
.prototype.onDetailViewButtonClick_ = function(event
) {
2137 this.setListType(FileManager
.ListType
.DETAIL
);
2138 this.currentList_
.focus();
2141 FileManager
.prototype.onThumbnailViewButtonClick_ = function(event
) {
2142 this.setListType(FileManager
.ListType
.THUMBNAIL
);
2143 this.currentList_
.focus();
2147 * KeyDown event handler for the document.
2149 FileManager
.prototype.onKeyDown_ = function(event
) {
2150 if (event
.srcElement
=== this.renameInput_
) {
2151 // Ignore keydown handler in the rename input box.
2155 switch (util
.getKeyModifiers(event
) + event
.keyCode
) {
2156 case 'Ctrl-17': // Ctrl => Show hidden setting
2157 this.dialogDom_
.setAttribute('ctrl-pressing', 'true');
2160 case 'Ctrl-190': // Ctrl-. => Toggle filter files.
2161 var dm
= this.directoryModel_
;
2162 dm
.setFilterHidden(!dm
.isFilterHiddenOn());
2163 event
.preventDefault();
2166 case '27': // Escape => Cancel dialog.
2167 if (this.copyManager_
&&
2168 this.copyManager_
.getStatus().totalFiles
!= 0) {
2169 // If there is a copy in progress, ESC will cancel it.
2170 event
.preventDefault();
2171 this.copyManager_
.requestCancel();
2175 if (this.butterBar_
&& this.butterBar_
.hideError()) {
2176 event
.preventDefault();
2180 if (this.dialogType
!= DialogType
.FULL_PAGE
) {
2181 // If there is nothing else for ESC to do, then cancel the dialog.
2182 event
.preventDefault();
2183 this.cancelButton_
.click();
2190 * KeyUp event handler for the document.
2192 FileManager
.prototype.onKeyUp_ = function(event
) {
2193 if (event
.srcElement
=== this.renameInput_
) {
2194 // Ignore keydown handler in the rename input box.
2198 switch (util
.getKeyModifiers(event
) + event
.keyCode
) {
2199 case '17': // Ctrl => Hide hidden setting
2200 this.dialogDom_
.removeAttribute('ctrl-pressing');
2206 * KeyDown event handler for the div#list-container element.
2208 FileManager
.prototype.onListKeyDown_ = function(event
) {
2209 if (event
.srcElement
.tagName
== 'INPUT') {
2210 // Ignore keydown handler in the rename input box.
2214 switch (util
.getKeyModifiers(event
) + event
.keyCode
) {
2215 case '8': // Backspace => Up one directory.
2216 event
.preventDefault();
2217 var path
= this.getCurrentDirectory();
2218 if (path
&& !PathUtil
.isRootPath(path
)) {
2219 var path
= path
.replace(/\/[^\/]+$/, '');
2220 this.directoryModel_
.changeDirectory(path
);
2224 case '13': // Enter => Change directory or perform default action.
2225 // TODO(dgozman): move directory action to dispatchSelectionAction.
2226 var selection
= this.getSelection();
2227 if (selection
.totalCount
== 1 &&
2228 selection
.entries
[0].isDirectory
&&
2229 this.dialogType
!= DialogType
.SELECT_FOLDER
) {
2230 event
.preventDefault();
2231 this.onDirectoryAction(selection
.entries
[0]);
2232 } else if (this.dispatchSelectionAction_()) {
2233 event
.preventDefault();
2238 switch (event
.keyIdentifier
) {
2245 // When navigating with keyboard we hide the distracting mouse hover
2246 // highlighting until the user moves the mouse again.
2247 this.setNoHover_(true);
2253 * Suppress/restore hover highlighting in the list container.
2254 * @param {boolean} on True to temporarity hide hover state.
2256 FileManager
.prototype.setNoHover_ = function(on
) {
2258 this.listContainer_
.classList
.add('nohover');
2260 this.listContainer_
.classList
.remove('nohover');
2265 * KeyPress event handler for the div#list-container element.
2267 FileManager
.prototype.onListKeyPress_ = function(event
) {
2268 if (event
.srcElement
.tagName
== 'INPUT') {
2269 // Ignore keypress handler in the rename input box.
2273 if (event
.ctrlKey
|| event
.metaKey
|| event
.altKey
)
2276 var now
= new Date();
2277 var char = String
.fromCharCode(event
.charCode
).toLowerCase();
2278 var text
= now
- this.textSearchState_
.date
> 1000 ? '' :
2279 this.textSearchState_
.text
;
2280 this.textSearchState_
= {text
: text
+ char, date
: now
};
2282 this.doTextSearch_();
2286 * Mousemove event handler for the div#list-container element.
2288 FileManager
.prototype.onListMouseMove_ = function(event
) {
2289 // The user grabbed the mouse, restore the hover highlighting.
2290 this.setNoHover_(false);
2294 * Performs a 'text search' - selects a first list entry with name
2295 * starting with entered text (case-insensitive).
2297 FileManager
.prototype.doTextSearch_ = function() {
2298 var text
= this.textSearchState_
.text
;
2302 var dm
= this.directoryModel_
.getFileList();
2303 for (var index
= 0; index
< dm
.length
; ++index
) {
2304 var name
= dm
.item(index
).name
;
2305 if (name
.substring(0, text
.length
).toLowerCase() == text
) {
2306 this.currentList_
.selectionModel
.selectedIndexes
= [index
];
2311 this.textSearchState_
.text
= '';
2315 * Handle a click of the cancel button. Closes the window.
2316 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
2318 * @param {Event} event The click event.
2320 FileManager
.prototype.onCancel_ = function(event
) {
2321 chrome
.fileBrowserPrivate
.cancelDialog();
2327 * Resolves selected file urls returned from an Open dialog.
2329 * For gdata files this involves some special treatment.
2330 * Starts getting gdata files if needed.
2332 * @param {Array.<string>} fileUrls GData URLs.
2333 * @param {function(Array.<string>)} callback To be called with fixed URLs.
2335 FileManager
.prototype.resolveSelectResults_ = function(fileUrls
, callback
) {
2336 if (this.isOnGData()) {
2337 chrome
.fileBrowserPrivate
.getDriveFiles(
2339 function(localPaths
) {
2340 fileUrls
= [].concat(fileUrls
); // Clone the array.
2341 // localPath can be empty if the file is not present, which
2342 // can happen if the user specifies a new file name to save a
2344 for (var i
= 0; i
!= localPaths
.length
; i
++) {
2345 if (localPaths
[i
]) {
2346 // Add "localPath" parameter to the gdata file URL.
2347 fileUrls
[i
] += '?localPath=' + encodeURIComponent(localPaths
[i
]);
2358 * Closes this modal dialog with some files selected.
2359 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
2360 * @param {Object} selection Contains urls, filterIndex and multiple fields.
2362 FileManager
.prototype.callSelectFilesApiAndClose_ = function(selection
) {
2363 if (selection
.multiple
) {
2364 chrome
.fileBrowserPrivate
.selectFiles(selection
.urls
);
2366 chrome
.fileBrowserPrivate
.selectFile(
2367 selection
.urls
[0], selection
.filterIndex
);
2374 * Tries to close this modal dialog with some files selected.
2375 * Performs preprocessing if needed (e.g. for GData).
2376 * @param {Object} selection Contains urls, filterIndex and multiple fields.
2378 FileManager
.prototype.selectFilesAndClose_ = function(selection
) {
2379 if (!this.isOnGData() ||
2380 this.dialogType
== DialogType
.SELECT_SAVEAS_FILE
) {
2381 setTimeout(this.callSelectFilesApiAndClose_
.bind(this, selection
), 0);
2385 var shade
= this.document_
.createElement('div');
2386 shade
.className
= 'shade';
2387 var footer
= this.document_
.querySelector('.dialog-footer');
2388 var progress
= footer
.querySelector('.progress-track');
2389 progress
.style
.width
= '0%';
2390 var cancelled
= false;
2392 var progressMap
= {};
2393 var filesStarted
= 0;
2394 var filesTotal
= selection
.urls
.length
;
2395 for (var index
= 0; index
< selection
.urls
.length
; index
++) {
2396 progressMap
[selection
.urls
[index
]] = -1;
2398 var lastPercent
= 0;
2402 var onFileTransfersUpdated = function(statusList
) {
2403 for (var index
= 0; index
< statusList
.length
; index
++) {
2404 var status
= statusList
[index
];
2405 var escaped
= encodeURI(status
.fileUrl
);
2406 if (!(escaped
in progressMap
)) continue;
2407 if (status
.total
== -1) continue;
2409 var old
= progressMap
[escaped
];
2411 // -1 means we don't know file size yet.
2412 bytesTotal
+= status
.total
;
2416 bytesDone
+= status
.processed
- old
;
2417 progressMap
[escaped
] = status
.processed
;
2420 var percent
= bytesTotal
== 0 ? 0 : bytesDone
/ bytesTotal
;
2421 // For files we don't have information about, assume the progress is zero.
2422 percent
= percent
* filesStarted
/ filesTotal
* 100;
2423 // Do not decrease the progress. This may happen, if first downloaded
2424 // file is small, and the second one is large.
2425 lastPercent
= Math
.max(lastPercent
, percent
);
2426 progress
.style
.width
= lastPercent
+ '%';
2429 var setup = function() {
2430 this.document_
.querySelector('.dialog-container').appendChild(shade
);
2431 setTimeout(function() { shade
.setAttribute('fadein', 'fadein') }, 100);
2432 footer
.setAttribute('progress', 'progress');
2433 this.cancelButton_
.removeEventListener('click', this.onCancelBound_
);
2434 this.cancelButton_
.addEventListener('click', onCancel
);
2435 chrome
.fileBrowserPrivate
.onFileTransfersUpdated
.addListener(
2436 onFileTransfersUpdated
);
2439 var cleanup = function() {
2440 shade
.parentNode
.removeChild(shade
);
2441 footer
.removeAttribute('progress');
2442 this.cancelButton_
.removeEventListener('click', onCancel
);
2443 this.cancelButton_
.addEventListener('click', this.onCancelBound_
);
2444 chrome
.fileBrowserPrivate
.onFileTransfersUpdated
.removeListener(
2445 onFileTransfersUpdated
);
2448 var onCancel = function() {
2450 // According to API cancel may fail, but there is no proper UI to reflect
2451 // this. So, we just silently assume that everything is cancelled.
2452 chrome
.fileBrowserPrivate
.cancelFileTransfers(
2453 selection
.urls
, function(response
) {});
2457 var onResolved = function(resolvedUrls
) {
2458 if (cancelled
) return;
2460 selection
.urls
= resolvedUrls
;
2461 // Call next method on a timeout, as it's unsafe to
2462 // close a window from a callback.
2463 setTimeout(this.callSelectFilesApiAndClose_
.bind(this, selection
), 0);
2466 var onProperties = function(properties
) {
2467 for (var i
= 0; i
< properties
.length
; i
++) {
2468 if (!properties
[i
] || properties
[i
].present
) {
2469 // For files already in GCache, we don't get any transfer updates.
2473 this.resolveSelectResults_(selection
.urls
, onResolved
);
2477 this.metadataCache_
.get(selection
.urls
, 'gdata', onProperties
);
2481 * Handle a click of the ok button.
2483 * The ok button has different UI labels depending on the type of dialog, but
2484 * in code it's always referred to as 'ok'.
2486 * @param {Event} event The click event.
2488 FileManager
.prototype.onOk_ = function(event
) {
2490 if (this.dialogType
== DialogType
.SELECT_SAVEAS_FILE
) {
2491 var currentDirUrl
= this.getCurrentDirectoryURL();
2493 if (currentDirUrl
.charAt(currentDirUrl
.length
- 1) != '/')
2494 currentDirUrl
+= '/';
2496 // Save-as doesn't require a valid selection from the list, since
2497 // we're going to take the filename from the text input.
2498 var filename
= this.filenameInput_
.value
;
2500 throw new Error('Missing filename!');
2501 if (!this.validateFileName_(filename
))
2504 var singleSelection
= {
2505 urls
: [currentDirUrl
+ encodeURIComponent(filename
)],
2507 filterIndex
: self
.getSelectedFilterIndex_(filename
)
2510 function resolveCallback(victim
) {
2511 if (victim
instanceof FileError
) {
2512 // File does not exist.
2513 self
.selectFilesAndClose_(singleSelection
);
2517 if (victim
.isDirectory
) {
2518 // Do not allow to overwrite directory.
2519 self
.alert
.show(strf('DIRECTORY_ALREADY_EXISTS', filename
));
2521 self
.confirm
.show(strf('CONFIRM_OVERWRITE_FILE', filename
),
2523 // User selected Ok from the confirm dialog.
2524 self
.selectFilesAndClose_(singleSelection
);
2529 this.resolvePath(this.getCurrentDirectory() + '/' + filename
,
2530 resolveCallback
, resolveCallback
);
2535 var selectedIndexes
= this.currentList_
.selectionModel
.selectedIndexes
;
2537 if (this.dialogType
== DialogType
.SELECT_FOLDER
&&
2538 selectedIndexes
.length
== 0) {
2539 var url
= this.getCurrentDirectoryURL();
2540 var singleSelection
= {
2543 filterIndex
: this.getSelectedFilterIndex_()
2545 this.selectFilesAndClose_(singleSelection
);
2549 // All other dialog types require at least one selected list item.
2550 // The logic to control whether or not the ok button is enabled should
2551 // prevent us from ever getting here, but we sanity check to be sure.
2552 if (!selectedIndexes
.length
)
2553 throw new Error('Nothing selected!');
2555 var dm
= this.directoryModel_
.getFileList();
2556 for (var i
= 0; i
< selectedIndexes
.length
; i
++) {
2557 var entry
= dm
.item(selectedIndexes
[i
]);
2559 console
.log('Error locating selected file at index: ' + i
);
2563 files
.push(entry
.toURL());
2566 // Multi-file selection has no other restrictions.
2567 if (this.dialogType
== DialogType
.SELECT_OPEN_MULTI_FILE
) {
2568 var multipleSelection
= {
2572 this.selectFilesAndClose_(multipleSelection
);
2576 // Everything else must have exactly one.
2577 if (files
.length
> 1)
2578 throw new Error('Too many files selected!');
2580 var selectedEntry
= dm
.item(selectedIndexes
[0]);
2582 if (this.dialogType
== DialogType
.SELECT_FOLDER
) {
2583 if (!selectedEntry
.isDirectory
)
2584 throw new Error('Selected entry is not a folder!');
2585 } else if (this.dialogType
== DialogType
.SELECT_OPEN_FILE
) {
2586 if (!selectedEntry
.isFile
)
2587 throw new Error('Selected entry is not a file!');
2590 var singleSelection
= {
2593 filterIndex
: this.getSelectedFilterIndex_()
2595 this.selectFilesAndClose_(singleSelection
);
2599 * Verifies the user entered name for file or folder to be created or
2600 * renamed to. Name restrictions must correspond to File API restrictions
2601 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is
2602 * out of date (spec is
2603 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to
2604 * be fixed. Shows message box if the name is invalid.
2606 * @param {name} name New file or folder name.
2607 * @param {function} opt_onDone Function to invoke when user closes the
2608 * warning box or immediatelly if file name is correct.
2609 * @return {boolean} True if name is vaild.
2611 FileManager
.prototype.validateFileName_ = function(name
, opt_onDone
) {
2612 var onDone
= opt_onDone
|| function() {};
2614 var testResult
= /[\/\\\<\>\:\?\*\"\|]/.exec(name
);
2616 msg
= strf('ERROR_INVALID_CHARACTER', testResult
[0]);
2617 } else if (/^\s*$/i.test(name
)) {
2618 msg
= str('ERROR_WHITESPACE_NAME');
2619 } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name
)) {
2620 msg
= str('ERROR_RESERVED_NAME');
2621 } else if (this.directoryModel_
.isFilterHiddenOn() && name
[0] == '.') {
2622 msg
= str('ERROR_HIDDEN_NAME');
2626 this.alert
.show(msg
, onDone
);
2635 * Handler invoked on preference setting in gdata context menu.
2636 * @param {string} pref The preference to alter.
2637 * @param {boolean} inverted Invert the value if true.
2638 * @param {Event} event The click event.
2640 FileManager
.prototype.onGDataPrefClick_ = function(pref
, inverted
, event
) {
2641 var newValue
= !event
.target
.hasAttribute('checked');
2643 event
.target
.setAttribute('checked', 'checked');
2645 event
.target
.removeAttribute('checked');
2647 var changeInfo
= {};
2648 changeInfo
[pref
] = inverted
? !newValue
: newValue
;
2649 chrome
.fileBrowserPrivate
.setPreferences(changeInfo
);
2652 FileManager
.prototype.onSearchBoxUpdate_ = function(event
) {
2653 var searchString
= this.document_
.getElementById('search-box').value
;
2654 var noResultsDiv
= this.document_
.getElementById('no-search-results');
2656 function reportEmptySearchResults() {
2657 if (this.directoryModel_
.getFileList().length
=== 0) {
2658 var text
= strf('SEARCH_NO_MATCHING_FILES', searchString
);
2659 noResultsDiv
.innerHTML
= text
;
2660 noResultsDiv
.setAttribute('show', 'true');
2662 noResultsDiv
.removeAttribute('show');
2666 function hideNoResultsDiv() {
2667 noResultsDiv
.removeAttribute('show');
2670 this.directoryModel_
.search(searchString
,
2671 reportEmptySearchResults
.bind(this),
2672 hideNoResultsDiv
.bind(this));
2675 FileManager
.prototype.decorateSplitter = function(splitterElement
) {
2678 var Splitter
= cr
.ui
.Splitter
;
2680 var customSplitter
= cr
.ui
.define('div');
2682 customSplitter
.prototype = {
2683 __proto__
: Splitter
.prototype,
2685 handleSplitterDragStart: function(e
) {
2686 Splitter
.prototype.handleSplitterDragStart
.apply(this, arguments
);
2687 this.ownerDocument
.documentElement
.classList
.add('col-resize');
2690 handleSplitterDragMove: function(deltaX
) {
2691 Splitter
.prototype.handleSplitterDragMove
.apply(this, arguments
);
2695 handleSplitterDragEnd: function(e
) {
2696 Splitter
.prototype.handleSplitterDragEnd
.apply(this, arguments
);
2697 this.ownerDocument
.documentElement
.classList
.remove('col-resize');
2701 customSplitter
.decorate(splitterElement
);
2705 * Listener invoked on gdata menu show event, to update gdata free/total
2706 * space info in opened menu.
2709 FileManager
.prototype.onGDataMenuShow_ = function() {
2710 var gdataSpaceInfoLabel
=
2711 this.dialogDom_
.querySelector('#gdata-space-info-label');
2713 var gdataSpaceInnerBar
=
2714 this.dialogDom_
.querySelector('#gdata-space-info-bar');
2715 var gdataSpaceOuterBar
=
2716 this.dialogDom_
.querySelector('#gdata-space-info-bar').parentNode
;
2718 gdataSpaceInnerBar
.setAttribute('pending', '');
2719 chrome
.fileBrowserPrivate
.getSizeStats(
2720 this.directoryModel_
.getCurrentRootUrl(), function(result
) {
2721 gdataSpaceInnerBar
.removeAttribute('pending');
2723 var sizeInGb
= util
.bytesToSi(result
.remainingSizeKB
* 1024);
2724 gdataSpaceInfoLabel
.textContent
=
2725 strf('DRIVE_SPACE_AVAILABLE', sizeInGb
);
2727 var usedSpace
= result
.totalSizeKB
- result
.remainingSizeKB
;
2728 gdataSpaceInnerBar
.style
.width
=
2729 (100 * usedSpace
/ result
.totalSizeKB
) + '%';
2731 gdataSpaceOuterBar
.style
.display
= '';
2733 gdataSpaceOuterBar
.style
.display
= 'none';
2734 gdataSpaceInfoLabel
.textContent
= str('DRIVE_FAILED_SPACE_INFO');
2740 * Updates default action menu item to match passed taskItem (icon,
2741 * label and action).
2743 * @param {Object} defaultItem - taskItem to match.
2744 * @param {boolean} isMultiple - if multiple tasks available.
2746 FileManager
.prototype.updateContextMenuActionItems = function(defaultItem
,
2749 if (defaultItem
.iconType
) {
2750 this.defaultActionMenuItem_
.style
.backgroundImage
= '';
2751 this.defaultActionMenuItem_
.setAttribute('file-type-icon',
2752 defaultItem
.iconType
);
2753 } else if (defaultItem
.iconUrl
) {
2754 this.defaultActionMenuItem_
.style
.backgroundImage
=
2755 'url(' + defaultItem
.iconUrl
+ ')';
2757 this.defaultActionMenuItem_
.style
.backgroundImage
= '';
2760 this.defaultActionMenuItem_
.label
= defaultItem
.title
;
2761 this.defaultActionMenuItem_
.taskId
= defaultItem
.taskId
;
2764 var defaultActionSeparator
=
2765 this.dialogDom_
.querySelector('#default-action-separator');
2767 this.openWithCommand_
.canExecuteChange();
2769 this.openWithCommand_
.setHidden(!(defaultItem
&& isMultiple
));
2770 this.defaultActionMenuItem_
.hidden
= !defaultItem
;
2771 defaultActionSeparator
.hidden
= !defaultItem
;
2776 * Window beforeunload handler.
2777 * @return {string} Message to show. Ignored when running as a packaged app.
2780 FileManager
.prototype.onBeforeUnload_ = function() {
2781 this.butterBar_
.forceDeleteAndHide();
2782 if (this.filePopup_
&&
2783 this.filePopup_
.contentWindow
&&
2784 this.filePopup_
.contentWindow
.beforeunload
) {
2785 // The gallery might want to prevent the unload if it is busy.
2786 return this.filePopup_
.contentWindow
.beforeunload();
2792 * @return {Selection} Selection object.
2794 FileManager
.prototype.getSelection = function() {
2795 return this.selectionHandler_
.selection
;
2799 * @return {ArrayDataModel} File list.
2801 FileManager
.prototype.getFileList = function() {
2802 return this.directoryModel_
.getFileList();
2806 * @return {cr.ui.List} Current list object.
2808 FileManager
.prototype.getCurrentList = function() {
2809 return this.currentList_
;