Updated drag and drop thumbnails.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / js / file_manager.js
blob62577aab1a2916497ef4be61bc95182ec91db733
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.
6 var JSErrorCount = 0;
8 /**
9 * Count uncaught exceptions.
11 window.onerror = function() { JSErrorCount++ };
13 /**
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).
20 * @constructor
21 * @param {HTMLElement} dialogDom The DOM node containing the prototypical
22 * dialog UI.
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;
31 util.saveAppState();
32 } else {
33 this.params_ = location.search ?
34 JSON.parse(decodeURIComponent(location.search.substr(1))) :
35 {};
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();
62 this.initDom_();
63 this.initDialogType_();
66 /**
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
70 * too long.
72 FileManager.THUMBNAIL_SHOW_DELAY = 100;
74 FileManager.prototype = {
75 __proto__: cr.EventTarget.prototype
78 /**
79 * Unload the file manager.
80 * Used by background.js (when running in the packaged mode).
82 function unload() {
83 fileManager.onBeforeUnload_();
84 fileManager.onUnload_();
87 /**
88 * List of dialog types.
90 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
91 * FULL_PAGE which is specific to this code.
93 * @enum {string}
95 var DialogType = {
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".
115 (function() {
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
144 * after the first.
146 var DOUBLE_CLICK_TIMEOUT = 200;
148 function removeChildren(element) {
149 element.textContent = '';
152 // Public statics.
154 FileManager.ListType = {
155 DETAIL: 'detail',
156 THUMBNAIL: 'thumb'
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.
190 * @override
191 * @param {?DirectoryEntry} entry New watched directory entry.
192 * @override
194 FileManager.MetadataFileWatcher.prototype.changeWatchedEntry = function(
195 entry) {
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;
207 if (!entry)
208 return;
210 this.filesystemObserverId_ = this.metadataCache_.addObserver(
211 entry,
212 MetadataCache.CHILDREN,
213 'filesystem',
214 this.filesystemChangeHandler_);
216 this.thumbnailObserverId_ = this.metadataCache_.addObserver(
217 entry,
218 MetadataCache.CHILDREN,
219 'thumbnail',
220 this.thumbnailChangeHandler_);
222 if (PathUtil.getRootType(entry.fullPath) === RootType.GDATA) {
223 this.gdataObserverId_ = this.metadataCache_.addObserver(
224 entry,
225 MetadataCache.CHILDREN,
226 'gdata',
227 this.gdataChangeHandler_);
230 this.internalObserverId_ = this.metadataCache_.addObserver(
231 entry,
232 MetadataCache.CHILDREN,
233 'internal',
234 this.internalChangeHandler_);
238 * @override
240 FileManager.MetadataFileWatcher.prototype.onFileInWatchedDirectoryChanged =
241 function() {
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;
252 if (callback)
253 callback();
258 * FileManager initially created hidden to prevent flickering.
259 * When DOM is almost constructed it need to be shown. Cancels
260 * delayed show.
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;
284 this.show_();
285 }.bind(this), delay);
289 // Instance methods.
292 * Request local file system, resolve roots and init_ after that.
293 * @private
295 FileManager.prototype.initFileSystem_ = function() {
296 util.installFileErrorToString();
297 // Replace the default unit in util to translated unit.
298 util.UNITS = [str('SIZE_KB'),
299 str('SIZE_MB'),
300 str('SIZE_GB'),
301 str('SIZE_TB'),
302 str('SIZE_PB')];
304 metrics.startInterval('Load.FileSystem');
306 var self = this;
307 var downcount = 3;
308 var viewOptions = {};
309 function done() {
310 if (--downcount == 0)
311 self.init_(viewOptions);
314 chrome.fileBrowserPrivate.requestLocalFileSystem(function(filesystem) {
315 metrics.recordInterval('Load.FileSystem');
316 self.filesystem_ = filesystem;
317 done();
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.
326 try {
327 viewOptions = JSON.parse(value);
328 } catch (ignore) {}
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];
336 done();
337 }.bind(this));
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();
350 }.bind(this));
352 this.initDateTimeFormatters_();
354 this.table_.startBatchUpdates();
355 this.grid_.startBatchUpdates();
357 this.initFileList_(prefs);
358 this.initDialogs_();
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));
373 var self = 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(
396 stateChangeHandler);
397 chrome.fileBrowserPrivate.onNetworkConnectionChanged.addListener(
398 stateChangeHandler);
399 stateChangeHandler();
401 this.refocus();
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.
418 this.delayShow_(0);
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_,
449 this.copyManager_,
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.
614 e.preventDefault();
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,
745 singleSelection,
746 this.metadataCache_,
747 this.volumeManager_,
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);
783 if (prefs.columns) {
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();
801 today.setHours(0);
802 today.setMinutes(0);
803 today.setSeconds(0);
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');
817 var self = this;
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;
831 var prefs = {
832 sortField: sortStatus.field,
833 sortDirection: sortStatus.direction,
834 columns: []
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;
848 util.saveAppState();
852 FileManager.prototype.refocus = function() {
853 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
854 this.filenameInput_.focus();
855 else
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)
865 return;
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.
877 return 0;
878 if (this.params_.includeAllFiles) // Already 1-based.
879 return index;
880 return index + 1; // Convert to 1-based;
883 FileManager.prototype.getRootEntry_ = function(index) {
884 if (index == -1)
885 return null;
887 return this.rootsList_.dataModel.item(index);
890 FileManager.prototype.setListType = function(type) {
891 if (type && type == this.listType_)
892 return;
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;
925 } else {
926 throw new Error('Unknown list type: ' + type);
929 this.listType_ = type;
930 this.updateStartupPrefs_();
931 this.onResize_();
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
943 // on ChromeOS.
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) {
953 this.alert.showHtml(
954 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
955 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
956 decodeURIComponent(
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
976 * update event).
978 FileManager.prototype.onCopyManagerOperationComplete_ = function(event) {
979 var currentPath = this.directoryModel_.getCurrentDirPath();
980 if (this.isOnGData() && this.directoryModel_.isSearching())
981 return;
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);
1004 option.value = 0;
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;
1011 if (!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.
1020 description = null;
1021 break;
1025 if (!description)
1026 // Convert ['jpg', 'png'] to '*.jpg, *.png'.
1027 description = fileType.extensions.map(function(s) {
1028 return '*.' + s;
1029 }).join(', ');
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;
1045 return;
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) {
1086 var g = this.grid_;
1087 g.startBatchUpdates();
1088 setTimeout(function() {
1089 g.columns = 0;
1090 g.redraw();
1091 g.endBatchUpdates();
1092 }, 0);
1093 } else {
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');
1108 } else {
1109 this.dialogDom_.removeAttribute('maximized');
1111 }.bind(this));
1114 FileManager.prototype.resolvePath = function(
1115 path, resultCallback, errorCallback) {
1116 return util.resolvePath(this.filesystem_.root, path, resultCallback,
1117 errorCallback);
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)) :
1134 this.defaultPath;
1136 if (!pageLoading && path == this.directoryModel_.getCurrentDirPath())
1137 return;
1139 if (!path) {
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;
1156 tracker.start();
1157 if (!this.isGDataEnabled()) {
1158 if (pageLoading)
1159 this.show_();
1160 var leafName = path.substr(path.indexOf('/') + 1);
1161 path = this.directoryModel_.getDefaultDirectory() + '/' + leafName;
1162 this.finishSetupCurrentDirectory_(path, invokeHandlers);
1163 return;
1165 var gdataPath = RootDirectory.GDATA;
1166 if (this.volumeManager_.isMounted(gdataPath)) {
1167 this.finishSetupCurrentDirectory_(path, invokeHandlers);
1168 return;
1170 if (pageLoading)
1171 this.delayShow_(500);
1172 // Reflect immediatelly in the UI we are on GData and display
1173 // mounting UI.
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');
1183 var self = this;
1184 this.volumeManager_.mountGData(function() {
1185 tracker.stop();
1186 if (!tracker.hasChanged) {
1187 self.finishSetupCurrentDirectory_(path, invokeHandlers);
1189 }, function(error) {
1190 tracker.stop();
1192 } else {
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) {
1208 var urls = null;
1209 var action = null;
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.
1216 urls = [];
1217 action = 'gallery';
1219 } else {
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)];
1229 action = 'gallery';
1231 if (FileType.getMediaType(path) == 'archive') {
1232 urls = [util.makeFilesystemUrl(path)];
1233 action = 'archives';
1237 if (urls) {
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);
1247 }.bind(this);
1248 this.directoryModel_.addEventListener('scan-completed', listener);
1251 if (action != 'gallery') {
1252 // Opening gallery will invoke |this.show_| at the right time.
1253 this.show_();
1255 }.bind(this);
1257 this.directoryModel_.setupPath(path, onResolve);
1258 return;
1261 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
1262 this.directoryModel_.setupPath(path, function(basePath, leafName) {
1263 this.filenameInput_.value = leafName;
1264 this.selectDefaultPathInFilenameInput_();
1265 }.bind(this));
1266 return;
1269 this.show_();
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() {
1278 var defaultTitle;
1279 var okLabel = str('OPEN_LABEL');
1281 switch (this.dialogType) {
1282 case DialogType.SELECT_FOLDER:
1283 defaultTitle = str('SELECT_FOLDER_TITLE');
1284 break;
1286 case DialogType.SELECT_OPEN_FILE:
1287 defaultTitle = str('SELECT_OPEN_FILE_TITLE');
1288 break;
1290 case DialogType.SELECT_OPEN_MULTI_FILE:
1291 defaultTitle = str('SELECT_OPEN_MULTI_FILE_TITLE');
1292 break;
1294 case DialogType.SELECT_SAVEAS_FILE:
1295 defaultTitle = str('SELECT_SAVEAS_FILE_TITLE');
1296 okLabel = str('SAVE_LABEL');
1297 break;
1299 case DialogType.FULL_PAGE:
1300 break;
1302 default:
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);
1350 }.bind(this));
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);
1366 return li;
1370 * Unmounts device.
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));
1376 if (listItem)
1377 listItem.setAttribute('disabled', '');
1378 var onError = function(error) {
1379 if (listItem)
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
1392 // changed.
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();
1414 var urls = [];
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];
1430 return map;
1431 }, {});
1433 if (this.listType_ == FileManager.ListType.DETAIL)
1434 this.table_.updateListItemsMetadata(type, propertyByUrl);
1435 else
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())
1450 return;
1452 var dm = this.directoryModel_;
1453 var leadIndex = dm.getFileListSelection().leadIndex;
1454 if (leadIndex < 0)
1455 return;
1457 var leadEntry = dm.getFileList().item(leadIndex);
1458 if (this.renameInput_.currentEntry.fullPath != leadEntry.fullPath)
1459 return;
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
1475 * closes it.
1476 * @param {Event} event Click event.
1478 FileManager.prototype.onExternalLinkClick_ = function(event) {
1479 if (event.target.tagName != 'A' || !event.target.href)
1480 return;
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) {
1490 this.onCancel_();
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);
1506 } else {
1507 var extensions = [];
1509 for (var i = 0; i < selection.urls.length; i++) {
1510 var match = /\.(\w+)$/g.exec(selection.urls[i]);
1511 if (match) {
1512 var ext = match[1].toUpperCase();
1513 if (extensions.indexOf(ext) == -1) {
1514 extensions.push(ext);
1519 var format = '';
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(
1551 callback) {
1552 var self = this;
1553 var downcount = 2;
1554 function done() {
1555 if (--downcount == 0)
1556 callback();
1559 chrome.fileBrowserPrivate.getPreferences(function(prefs) {
1560 self.preferences_ = prefs;
1561 done();
1564 chrome.fileBrowserPrivate.getNetworkConnectionState(function(networkState) {
1565 self.networkState_ = networkState;
1566 done();
1570 FileManager.prototype.onNetworkStateOrPreferencesChanged_ = function() {
1571 var self = this;
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', '');
1584 else
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', '');
1594 else
1595 self.hostedButton.removeAttribute('checked');
1597 if (network.online) {
1598 if (gdata.cellularDisabled && network.type == 'cellular')
1599 self.dialogContainer_.setAttribute('connection', 'metered');
1600 else
1601 self.dialogContainer_.removeAttribute('connection');
1602 } else {
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;
1671 this.refocus();
1675 FileManager.prototype.getAllUrlsInCurrentDirectory = function() {
1676 var urls = [];
1677 var fileList = this.directoryModel_.getFileList();
1678 for (var i = 0; i != fileList.length; i++) {
1679 urls.push(fileList.item(i).toURL());
1681 return urls;
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)
1692 this.grid_.focus();
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)
1718 return;
1720 for (var i = 0; i < selection.entries.length; i++) {
1721 var selectedIndex = selection.indexes[i];
1722 var listItem = this.currentList_.getListItemByIndex(selectedIndex);
1723 if (listItem)
1724 this.blinkListItem_(listItem);
1728 FileManager.prototype.blinkListItem_ = function(listItem) {
1729 listItem.classList.add('blink');
1730 setTimeout(function() {
1731 listItem.classList.remove('blink');
1732 }, 100);
1735 FileManager.prototype.selectDefaultPathInFilenameInput_ = function() {
1736 var input = this.filenameInput_;
1737 input.focus();
1738 var selectionEnd = input.value.lastIndexOf('.');
1739 if (selectionEnd == -1) {
1740 input.select();
1741 } else {
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.
1756 return;
1759 var listItem = this.findListItemForEvent_(event);
1760 var selection = this.getSelection();
1761 if (!listItem || !listItem.selected || selection.totalCount != 1) {
1762 return;
1765 var clickNumber;
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.
1770 clickNumber = 1;
1771 } else if (this.lastClickedItem_ == listItem) {
1772 // React on double click, but only if both clicks hit the same item.
1773 clickNumber = 2;
1775 this.lastClickedItem_ = listItem;
1777 if (event.detail != clickNumber)
1778 return;
1780 var entry = selection.entries[0];
1781 if (entry.isDirectory) {
1782 this.onDirectoryAction(entry);
1783 } else {
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();
1792 return true;
1794 if (!this.okButton_.disabled) {
1795 this.onOk_();
1796 return true;
1798 return false;
1802 * Executes directory action (i.e. changes directory).
1804 * @param {DirectoryEntry} entry Directory entry to which directory should be
1805 * changed.
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)
1824 return;
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');
1875 } else {
1876 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
1878 } else {
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
1896 * return.
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();
1909 if (!item)
1910 return;
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);
1917 input.focus();
1918 var selectionEnd = input.value.lastIndexOf('.');
1919 if (selectionEnd == -1) {
1920 input.select();
1921 } else {
1922 input.selectionStart = 0;
1923 input.selectionEnd = selectionEnd;
1926 // This has to be set late in the process so we don't handle spurious
1927 // blur events.
1928 input.currentEntry = this.currentList_.dataModel.item(item.listIndex);
1931 FileManager.prototype.onRenameInputKeyDown_ = function(event) {
1932 if (!this.isRenamingInProgress())
1933 return;
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();
1944 break;
1946 case '13': // Enter
1947 this.commitRename_();
1948 event.preventDefault();
1949 break;
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_();
1965 return;
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)))
1980 return;
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
1989 // case of success.
1990 nameNode.textContent = newName;
1992 this.directoryModel_.doesExist(entry, newName, function(exists, isFile) {
1993 if (!exists) {
1994 this.directoryModel_.renameEntry(entry, newName, onError.bind(this));
1995 } else {
1996 nameNode.textContent = entry.name;
1997 var message = isFile ? 'FILE_ALREADY_EXISTS' :
1998 'DIRECTORY_ALREADY_EXISTS';
1999 this.alert.show(strf(message, newName));
2001 }.bind(this));
2004 FileManager.prototype.cancelRename_ = function() {
2005 this.renameInput_.currentEntry = null;
2007 var parent = this.renameInput_.parentNode;
2008 if (parent) {
2009 parent.removeAttribute('renaming');
2010 parent.removeChild(this.renameInput_);
2012 this.refocus();
2015 FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
2016 var enabled = this.selectionHandler_.updateOkButton();
2017 if (enabled &&
2018 (util.getKeyModifiers(event) + event.keyCode) == '13' /* Enter */)
2019 this.onOk_();
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) {
2031 input.select();
2032 } else {
2033 input.selectionStart = 0;
2034 input.selectionEnd = selectionEnd;
2036 }, 0);
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';
2068 } else {
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();
2086 var hash = {};
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
2090 // and '__proto__'.
2091 if (name.substring(0, defaultName.length) == defaultName)
2092 hash[name] = 1;
2095 var baseName = defaultName;
2096 var separator = '';
2097 var suffix = '';
2098 var index = '';
2100 function advance() {
2101 separator = ' (';
2102 suffix = ')';
2103 index++;
2106 function current() {
2107 return baseName + separator + index + suffix;
2110 // Accessing hasOwnProperty is safe since hash properties filtered.
2111 while (hash.hasOwnProperty(current())) {
2112 advance();
2115 var self = this;
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)));
2133 tryCreate();
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.
2152 return;
2155 switch (util.getKeyModifiers(event) + event.keyCode) {
2156 case 'Ctrl-17': // Ctrl => Show hidden setting
2157 this.dialogDom_.setAttribute('ctrl-pressing', 'true');
2158 return;
2160 case 'Ctrl-190': // Ctrl-. => Toggle filter files.
2161 var dm = this.directoryModel_;
2162 dm.setFilterHidden(!dm.isFilterHiddenOn());
2163 event.preventDefault();
2164 return;
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();
2172 return;
2175 if (this.butterBar_ && this.butterBar_.hideError()) {
2176 event.preventDefault();
2177 return;
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();
2185 break;
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.
2195 return;
2198 switch (util.getKeyModifiers(event) + event.keyCode) {
2199 case '17': // Ctrl => Hide hidden setting
2200 this.dialogDom_.removeAttribute('ctrl-pressing');
2201 return;
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.
2211 return;
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);
2222 break;
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();
2235 break;
2238 switch (event.keyIdentifier) {
2239 case 'Home':
2240 case 'End':
2241 case 'Up':
2242 case 'Down':
2243 case 'Left':
2244 case 'Right':
2245 // When navigating with keyboard we hide the distracting mouse hover
2246 // highlighting until the user moves the mouse again.
2247 this.setNoHover_(true);
2248 break;
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) {
2257 if (on) {
2258 this.listContainer_.classList.add('nohover');
2259 } else {
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.
2270 return;
2273 if (event.ctrlKey || event.metaKey || event.altKey)
2274 return;
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;
2299 if (!text)
2300 return;
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];
2307 return;
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();
2322 this.onUnload_();
2323 window.close();
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(
2338 fileUrls,
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
2343 // file on gdata.
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]);
2350 callback(fileUrls);
2352 } else {
2353 callback(fileUrls);
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);
2365 } else {
2366 chrome.fileBrowserPrivate.selectFile(
2367 selection.urls[0], selection.filterIndex);
2369 this.onUnload_();
2370 window.close();
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);
2382 return;
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;
2399 var bytesTotal = 0;
2400 var bytesDone = 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];
2410 if (old == -1) {
2411 // -1 means we don't know file size yet.
2412 bytesTotal += status.total;
2413 filesStarted++;
2414 old = 0;
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 + '%';
2427 }.bind(this);
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);
2437 }.bind(this);
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);
2446 }.bind(this);
2448 var onCancel = function() {
2449 cancelled = true;
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) {});
2454 cleanup();
2455 }.bind(this);
2457 var onResolved = function(resolvedUrls) {
2458 if (cancelled) return;
2459 cleanup();
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);
2464 }.bind(this);
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.
2470 filesTotal--;
2473 this.resolveSelectResults_(selection.urls, onResolved);
2474 }.bind(this);
2476 setup();
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) {
2489 var self = this;
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;
2499 if (!filename)
2500 throw new Error('Missing filename!');
2501 if (!this.validateFileName_(filename))
2502 return;
2504 var singleSelection = {
2505 urls: [currentDirUrl + encodeURIComponent(filename)],
2506 multiple: false,
2507 filterIndex: self.getSelectedFilterIndex_(filename)
2510 function resolveCallback(victim) {
2511 if (victim instanceof FileError) {
2512 // File does not exist.
2513 self.selectFilesAndClose_(singleSelection);
2514 return;
2517 if (victim.isDirectory) {
2518 // Do not allow to overwrite directory.
2519 self.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
2520 } else {
2521 self.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
2522 function() {
2523 // User selected Ok from the confirm dialog.
2524 self.selectFilesAndClose_(singleSelection);
2529 this.resolvePath(this.getCurrentDirectory() + '/' + filename,
2530 resolveCallback, resolveCallback);
2531 return;
2534 var files = [];
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 = {
2541 urls: [url],
2542 multiple: false,
2543 filterIndex: this.getSelectedFilterIndex_()
2545 this.selectFilesAndClose_(singleSelection);
2546 return;
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]);
2558 if (!entry) {
2559 console.log('Error locating selected file at index: ' + i);
2560 continue;
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 = {
2569 urls: files,
2570 multiple: true
2572 this.selectFilesAndClose_(multipleSelection);
2573 return;
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 = {
2591 urls: [files[0]],
2592 multiple: false,
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() {};
2613 var msg;
2614 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
2615 if (testResult) {
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');
2625 if (msg) {
2626 this.alert.show(msg, onDone);
2627 return false;
2630 onDone();
2631 return true;
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');
2642 if (newValue)
2643 event.target.setAttribute('checked', 'checked');
2644 else
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');
2661 } else {
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) {
2676 var self = this;
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);
2692 self.onResize_();
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.
2707 * @private
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');
2722 if (result) {
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 = '';
2732 } else {
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,
2747 isMultiple) {
2748 if (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 + ')';
2756 } else {
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.
2778 * @private
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();
2788 return null;
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_;
2811 })();