Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(157)

Side by Side Diff: chrome/browser/resources/file_manager/foreground/js/file_manager.js

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

Powered by Google App Engine
This is Rietveld 408576698