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

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

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

Powered by Google App Engine
This is Rietveld 408576698