OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 'use strict'; | |
6 | |
7 // If directory files changes too often, don't rescan directory more than once | |
8 // per specified interval | |
9 var SIMULTANEOUS_RESCAN_INTERVAL = 1000; | |
10 // Used for operations that require almost instant rescan. | |
11 var SHORT_RESCAN_INTERVAL = 100; | |
12 | |
13 /** | |
14 * Data model of the file manager. | |
15 * | |
16 * @param {boolean} singleSelection True if only one file could be selected | |
17 * at the time. | |
18 * @param {FileFilter} fileFilter Instance of FileFilter. | |
19 * @param {FileWatcher} fileWatcher Instance of FileWatcher. | |
20 * @param {MetadataCache} metadataCache The metadata cache service. | |
21 * @param {VolumeManagerWrapper} volumeManager The volume manager. | |
22 * @param {boolean} showSpecialSearchRoots True if special-search roots are | |
23 * available. They should be hidden for the dialogs to save files. | |
24 * @constructor | |
25 */ | |
26 function DirectoryModel(singleSelection, fileFilter, fileWatcher, | |
27 metadataCache, volumeManager, | |
28 showSpecialSearchRoots) { | |
29 this.fileListSelection_ = singleSelection ? | |
30 new cr.ui.ListSingleSelectionModel() : new cr.ui.ListSelectionModel(); | |
31 | |
32 this.runningScan_ = null; | |
33 this.pendingScan_ = null; | |
34 this.rescanTime_ = null; | |
35 this.scanFailures_ = 0; | |
36 this.showSpecialSearchRoots_ = showSpecialSearchRoots; | |
37 | |
38 this.fileFilter_ = fileFilter; | |
39 this.fileFilter_.addEventListener('changed', | |
40 this.onFilterChanged_.bind(this)); | |
41 | |
42 this.currentFileListContext_ = new FileListContext( | |
43 fileFilter, metadataCache); | |
44 this.currentDirContents_ = | |
45 DirectoryContents.createForDirectory(this.currentFileListContext_, null); | |
46 | |
47 this.volumeManager_ = volumeManager; | |
48 this.volumeManager_.volumeInfoList.addEventListener( | |
49 'splice', this.onVolumeInfoListUpdated_.bind(this)); | |
50 | |
51 this.fileWatcher_ = fileWatcher; | |
52 this.fileWatcher_.addEventListener( | |
53 'watcher-directory-changed', | |
54 this.onWatcherDirectoryChanged_.bind(this)); | |
55 } | |
56 | |
57 /** | |
58 * Fake entry to be used in currentDirEntry_ when current directory is | |
59 * unmounted DRIVE. TODO(haruki): Support "drive/root" and "drive/other". | |
60 * @type {Object} | |
61 * @const | |
62 * @private | |
63 */ | |
64 DirectoryModel.fakeDriveEntry_ = { | |
65 fullPath: RootDirectory.DRIVE + '/' + DriveSubRootDirectory.ROOT, | |
66 isDirectory: true | |
67 }; | |
68 | |
69 /** | |
70 * Fake entry representing a psuedo directory, which contains Drive files | |
71 * available offline. This entry works as a trigger to start a search for | |
72 * offline files. | |
73 * @type {Object} | |
74 * @const | |
75 * @private | |
76 */ | |
77 DirectoryModel.fakeDriveOfflineEntry_ = { | |
78 fullPath: RootDirectory.DRIVE_OFFLINE, | |
79 isDirectory: true | |
80 }; | |
81 | |
82 /** | |
83 * Fake entry representing a pseudo directory, which contains shared-with-me | |
84 * Drive files. This entry works as a trigger to start a search for | |
85 * shared-with-me files. | |
86 * @type {Object} | |
87 * @const | |
88 * @private | |
89 */ | |
90 DirectoryModel.fakeDriveSharedWithMeEntry_ = { | |
91 fullPath: RootDirectory.DRIVE_SHARED_WITH_ME, | |
92 isDirectory: true | |
93 }; | |
94 | |
95 /** | |
96 * Fake entry representing a pseudo directory, which contains Drive files | |
97 * accessed recently. This entry works as a trigger to start a metadata search | |
98 * implemented as DirectoryContentsDriveRecent. | |
99 * DirectoryModel is responsible to start the search when the UI tries to open | |
100 * this fake entry (e.g. changeDirectory()). | |
101 * @type {Object} | |
102 * @const | |
103 * @private | |
104 */ | |
105 DirectoryModel.fakeDriveRecentEntry_ = { | |
106 fullPath: RootDirectory.DRIVE_RECENT, | |
107 isDirectory: true | |
108 }; | |
109 | |
110 /** | |
111 * List of fake entries for special searches. | |
112 * | |
113 * @type {Array.<Object>} | |
114 * @const | |
115 */ | |
116 DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES = [ | |
117 DirectoryModel.fakeDriveSharedWithMeEntry_, | |
118 DirectoryModel.fakeDriveRecentEntry_, | |
119 DirectoryModel.fakeDriveOfflineEntry_ | |
120 ]; | |
121 | |
122 /** | |
123 * DirectoryModel extends cr.EventTarget. | |
124 */ | |
125 DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype; | |
126 | |
127 /** | |
128 * Disposes the directory model by removing file watchers. | |
129 */ | |
130 DirectoryModel.prototype.dispose = function() { | |
131 this.fileWatcher_.dispose(); | |
132 }; | |
133 | |
134 /** | |
135 * @return {cr.ui.ArrayDataModel} Files in the current directory. | |
136 */ | |
137 DirectoryModel.prototype.getFileList = function() { | |
138 return this.currentFileListContext_.fileList; | |
139 }; | |
140 | |
141 /** | |
142 * Sort the file list. | |
143 * @param {string} sortField Sort field. | |
144 * @param {string} sortDirection "asc" or "desc". | |
145 */ | |
146 DirectoryModel.prototype.sortFileList = function(sortField, sortDirection) { | |
147 this.getFileList().sort(sortField, sortDirection); | |
148 }; | |
149 | |
150 /** | |
151 * @return {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} Selection | |
152 * in the fileList. | |
153 */ | |
154 DirectoryModel.prototype.getFileListSelection = function() { | |
155 return this.fileListSelection_; | |
156 }; | |
157 | |
158 /** | |
159 * @return {RootType} Root type of current root. | |
160 */ | |
161 DirectoryModel.prototype.getCurrentRootType = function() { | |
162 var entry = this.currentDirContents_.getDirectoryEntry(); | |
163 return PathUtil.getRootType(entry ? entry.fullPath : ''); | |
164 }; | |
165 | |
166 /** | |
167 * @return {string} Root path. | |
168 */ | |
169 DirectoryModel.prototype.getCurrentRootPath = function() { | |
170 var entry = this.currentDirContents_.getDirectoryEntry(); | |
171 return entry ? PathUtil.getRootPath(entry.fullPath) : ''; | |
172 }; | |
173 | |
174 /** | |
175 * @return {string} Filesystem URL representing the mountpoint for the current | |
176 * contents. | |
177 */ | |
178 DirectoryModel.prototype.getCurrentMountPointUrl = function() { | |
179 var rootPath = this.getCurrentRootPath(); | |
180 // Special search roots are just showing a search results from DRIVE. | |
181 if (PathUtil.getRootType(rootPath) == RootType.DRIVE || | |
182 PathUtil.isSpecialSearchRoot(rootPath)) | |
183 return util.makeFilesystemUrl(RootDirectory.DRIVE); | |
184 | |
185 return util.makeFilesystemUrl(rootPath); | |
186 }; | |
187 | |
188 /** | |
189 * @return {boolean} on True if offline. | |
190 */ | |
191 DirectoryModel.prototype.isDriveOffline = function() { | |
192 var connection = this.volumeManager_.getDriveConnectionState(); | |
193 return connection.type == util.DriveConnectionType.OFFLINE; | |
194 }; | |
195 | |
196 /** | |
197 * TODO(haruki): This actually checks the current root. Fix the method name and | |
198 * related code. | |
199 * @return {boolean} True if the root for the current directory is read only. | |
200 */ | |
201 DirectoryModel.prototype.isReadOnly = function() { | |
202 return this.isPathReadOnly(this.getCurrentRootPath()); | |
203 }; | |
204 | |
205 /** | |
206 * @return {boolean} True if the a scan is active. | |
207 */ | |
208 DirectoryModel.prototype.isScanning = function() { | |
209 return this.currentDirContents_.isScanning(); | |
210 }; | |
211 | |
212 /** | |
213 * @return {boolean} True if search is in progress. | |
214 */ | |
215 DirectoryModel.prototype.isSearching = function() { | |
216 return this.currentDirContents_.isSearch(); | |
217 }; | |
218 | |
219 /** | |
220 * @param {string} path Path to check. | |
221 * @return {boolean} True if the |path| is read only. | |
222 */ | |
223 DirectoryModel.prototype.isPathReadOnly = function(path) { | |
224 // TODO(hidehiko): Migrate this into VolumeInfo. | |
225 switch (PathUtil.getRootType(path)) { | |
226 case RootType.REMOVABLE: | |
227 var volumeInfo = this.volumeManager_.getVolumeInfo( | |
228 PathUtil.getRootPath(path)); | |
229 // Returns true if the volume is actually read only, or if an error | |
230 // is found during the mounting. | |
231 // TODO(hidehiko): Remove "error" check here, by removing error'ed volume | |
232 // info from VolumeManager. | |
233 return volumeInfo && (volumeInfo.isReadOnly || !!volumeInfo.error); | |
234 case RootType.ARCHIVE: | |
235 return true; | |
236 case RootType.DOWNLOADS: | |
237 return false; | |
238 case RootType.DRIVE: | |
239 // TODO(haruki): Maybe add DRIVE_OFFLINE as well to allow renaming in the | |
240 // offline tab. | |
241 return this.isDriveOffline(); | |
242 default: | |
243 return true; | |
244 } | |
245 }; | |
246 | |
247 /** | |
248 * Updates the selection by using the updateFunc and publish the change event. | |
249 * If updateFunc returns true, it force to dispatch the change event even if the | |
250 * selection index is not changed. | |
251 * | |
252 * @param {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} selection | |
253 * Selection to be updated. | |
254 * @param {function(): boolean} updateFunc Function updating the selection. | |
255 * @private | |
256 */ | |
257 DirectoryModel.prototype.updateSelectionAndPublishEvent_ = | |
258 function(selection, updateFunc) { | |
259 // Begin change. | |
260 selection.beginChange(); | |
261 | |
262 // If dispatchNeeded is true, we should ensure the change event is | |
263 // dispatched. | |
264 var dispatchNeeded = updateFunc(); | |
265 | |
266 // Check if the change event is dispatched in the endChange function | |
267 // or not. | |
268 var eventDispatched = function() { dispatchNeeded = false; }; | |
269 selection.addEventListener('change', eventDispatched); | |
270 selection.endChange(); | |
271 selection.removeEventListener('change', eventDispatched); | |
272 | |
273 // If the change event have been already dispatched, dispatchNeeded is false. | |
274 if (dispatchNeeded) { | |
275 var event = new Event('change'); | |
276 // The selection status (selected or not) is not changed because | |
277 // this event is caused by the change of selected item. | |
278 event.changes = []; | |
279 selection.dispatchEvent(event); | |
280 } | |
281 }; | |
282 | |
283 /** | |
284 * Invoked when a change in the directory is detected by the watcher. | |
285 * @private | |
286 */ | |
287 DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() { | |
288 this.rescanSoon(); | |
289 }; | |
290 | |
291 /** | |
292 * Invoked when filters are changed. | |
293 * @private | |
294 */ | |
295 DirectoryModel.prototype.onFilterChanged_ = function() { | |
296 this.rescanSoon(); | |
297 }; | |
298 | |
299 /** | |
300 * Returns the filter. | |
301 * @return {FileFilter} The file filter. | |
302 */ | |
303 DirectoryModel.prototype.getFileFilter = function() { | |
304 return this.fileFilter_; | |
305 }; | |
306 | |
307 /** | |
308 * @return {DirectoryEntry} Current directory. | |
309 */ | |
310 DirectoryModel.prototype.getCurrentDirEntry = function() { | |
311 return this.currentDirContents_.getDirectoryEntry(); | |
312 }; | |
313 | |
314 /** | |
315 * @return {string} URL of the current directory. or null if unavailable. | |
316 */ | |
317 DirectoryModel.prototype.getCurrentDirectoryURL = function() { | |
318 var entry = this.currentDirContents_.getDirectoryEntry(); | |
319 if (!entry) | |
320 return null; | |
321 if (entry === DirectoryModel.fakeDriveOfflineEntry_) | |
322 return util.makeFilesystemUrl(entry.fullPath); | |
323 return entry.toURL(); | |
324 }; | |
325 | |
326 /** | |
327 * @return {string} Path for the current directory, or empty string if the | |
328 * current directory is not yet set. | |
329 */ | |
330 DirectoryModel.prototype.getCurrentDirPath = function() { | |
331 var entry = this.currentDirContents_.getDirectoryEntry(); | |
332 return entry ? entry.fullPath : ''; | |
333 }; | |
334 | |
335 /** | |
336 * @return {Array.<string>} File paths of selected files. | |
337 * @private | |
338 */ | |
339 DirectoryModel.prototype.getSelectedPaths_ = function() { | |
340 var indexes = this.fileListSelection_.selectedIndexes; | |
341 var fileList = this.getFileList(); | |
342 if (fileList) { | |
343 return indexes.map(function(i) { | |
344 return fileList.item(i).fullPath; | |
345 }); | |
346 } | |
347 return []; | |
348 }; | |
349 | |
350 /** | |
351 * @param {Array.<string>} value List of file paths of selected files. | |
352 * @private | |
353 */ | |
354 DirectoryModel.prototype.setSelectedPaths_ = function(value) { | |
355 var indexes = []; | |
356 var fileList = this.getFileList(); | |
357 | |
358 var safeKey = function(key) { | |
359 // The transformation must: | |
360 // 1. Never generate a reserved name ('__proto__') | |
361 // 2. Keep different keys different. | |
362 return '#' + key; | |
363 }; | |
364 | |
365 var hash = {}; | |
366 | |
367 for (var i = 0; i < value.length; i++) | |
368 hash[safeKey(value[i])] = 1; | |
369 | |
370 for (var i = 0; i < fileList.length; i++) { | |
371 if (hash.hasOwnProperty(safeKey(fileList.item(i).fullPath))) | |
372 indexes.push(i); | |
373 } | |
374 this.fileListSelection_.selectedIndexes = indexes; | |
375 }; | |
376 | |
377 /** | |
378 * @return {string} Lead item file path. | |
379 * @private | |
380 */ | |
381 DirectoryModel.prototype.getLeadPath_ = function() { | |
382 var index = this.fileListSelection_.leadIndex; | |
383 return index >= 0 && this.getFileList().item(index).fullPath; | |
384 }; | |
385 | |
386 /** | |
387 * @param {string} value The name of new lead index. | |
388 * @private | |
389 */ | |
390 DirectoryModel.prototype.setLeadPath_ = function(value) { | |
391 var fileList = this.getFileList(); | |
392 for (var i = 0; i < fileList.length; i++) { | |
393 if (fileList.item(i).fullPath === value) { | |
394 this.fileListSelection_.leadIndex = i; | |
395 return; | |
396 } | |
397 } | |
398 }; | |
399 | |
400 /** | |
401 * Schedule rescan with short delay. | |
402 */ | |
403 DirectoryModel.prototype.rescanSoon = function() { | |
404 this.scheduleRescan(SHORT_RESCAN_INTERVAL); | |
405 }; | |
406 | |
407 /** | |
408 * Schedule rescan with delay. Designed to handle directory change | |
409 * notification. | |
410 */ | |
411 DirectoryModel.prototype.rescanLater = function() { | |
412 this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL); | |
413 }; | |
414 | |
415 /** | |
416 * Schedule rescan with delay. If another rescan has been scheduled does | |
417 * nothing. File operation may cause a few notifications what should cause | |
418 * a single refresh. | |
419 * @param {number} delay Delay in ms after which the rescan will be performed. | |
420 */ | |
421 DirectoryModel.prototype.scheduleRescan = function(delay) { | |
422 if (this.rescanTime_) { | |
423 if (this.rescanTime_ <= Date.now() + delay) | |
424 return; | |
425 clearTimeout(this.rescanTimeoutId_); | |
426 } | |
427 | |
428 this.rescanTime_ = Date.now() + delay; | |
429 this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay); | |
430 }; | |
431 | |
432 /** | |
433 * Cancel a rescan on timeout if it is scheduled. | |
434 * @private | |
435 */ | |
436 DirectoryModel.prototype.clearRescanTimeout_ = function() { | |
437 this.rescanTime_ = null; | |
438 if (this.rescanTimeoutId_) { | |
439 clearTimeout(this.rescanTimeoutId_); | |
440 this.rescanTimeoutId_ = null; | |
441 } | |
442 }; | |
443 | |
444 /** | |
445 * Rescan current directory. May be called indirectly through rescanLater or | |
446 * directly in order to reflect user action. Will first cache all the directory | |
447 * contents in an array, then seamlessly substitute the fileList contents, | |
448 * preserving the select element etc. | |
449 * | |
450 * This should be to scan the contents of current directory (or search). | |
451 */ | |
452 DirectoryModel.prototype.rescan = function() { | |
453 this.clearRescanTimeout_(); | |
454 if (this.runningScan_) { | |
455 this.pendingRescan_ = true; | |
456 return; | |
457 } | |
458 | |
459 var dirContents = this.currentDirContents_.clone(); | |
460 dirContents.setFileList([]); | |
461 | |
462 var successCallback = (function() { | |
463 this.replaceDirectoryContents_(dirContents); | |
464 cr.dispatchSimpleEvent(this, 'rescan-completed'); | |
465 }).bind(this); | |
466 | |
467 this.scan_(dirContents, | |
468 successCallback, function() {}, function() {}, function() {}); | |
469 }; | |
470 | |
471 /** | |
472 * Run scan on the current DirectoryContents. The active fileList is cleared and | |
473 * the entries are added directly. | |
474 * | |
475 * This should be used when changing directory or initiating a new search. | |
476 * | |
477 * @param {DirectoryContentes} newDirContents New DirectoryContents instance to | |
478 * replace currentDirContents_. | |
479 * @param {function()=} opt_callback Called on success. | |
480 * @private | |
481 */ | |
482 DirectoryModel.prototype.clearAndScan_ = function(newDirContents, | |
483 opt_callback) { | |
484 if (this.currentDirContents_.isScanning()) | |
485 this.currentDirContents_.cancelScan(); | |
486 this.currentDirContents_ = newDirContents; | |
487 this.clearRescanTimeout_(); | |
488 | |
489 if (this.pendingScan_) | |
490 this.pendingScan_ = false; | |
491 | |
492 if (this.runningScan_) { | |
493 if (this.runningScan_.isScanning()) | |
494 this.runningScan_.cancelScan(); | |
495 this.runningScan_ = null; | |
496 } | |
497 | |
498 var onDone = function() { | |
499 cr.dispatchSimpleEvent(this, 'scan-completed'); | |
500 if (opt_callback) | |
501 opt_callback(); | |
502 }.bind(this); | |
503 | |
504 var onFailed = function() { | |
505 cr.dispatchSimpleEvent(this, 'scan-failed'); | |
506 }.bind(this); | |
507 | |
508 var onUpdated = function() { | |
509 cr.dispatchSimpleEvent(this, 'scan-updated'); | |
510 }.bind(this); | |
511 | |
512 var onCancelled = function() { | |
513 cr.dispatchSimpleEvent(this, 'scan-cancelled'); | |
514 }.bind(this); | |
515 | |
516 // Clear the table, and start scanning. | |
517 cr.dispatchSimpleEvent(this, 'scan-started'); | |
518 var fileList = this.getFileList(); | |
519 fileList.splice(0, fileList.length); | |
520 this.scan_(this.currentDirContents_, | |
521 onDone, onFailed, onUpdated, onCancelled); | |
522 }; | |
523 | |
524 /** | |
525 * Perform a directory contents scan. Should be called only from rescan() and | |
526 * clearAndScan_(). | |
527 * | |
528 * @param {DirectoryContents} dirContents DirectoryContents instance on which | |
529 * the scan will be run. | |
530 * @param {function()} successCallback Callback on success. | |
531 * @param {function()} failureCallback Callback on failure. | |
532 * @param {function()} updatedCallback Callback on update. Only on the last | |
533 * update, {@code successCallback} is called instead of this. | |
534 * @param {function()} cancelledCallback Callback on cancel. | |
535 * @private | |
536 */ | |
537 DirectoryModel.prototype.scan_ = function( | |
538 dirContents, | |
539 successCallback, failureCallback, updatedCallback, cancelledCallback) { | |
540 var self = this; | |
541 | |
542 /** | |
543 * Runs pending scan if there is one. | |
544 * | |
545 * @return {boolean} Did pending scan exist. | |
546 */ | |
547 var maybeRunPendingRescan = function() { | |
548 if (self.pendingRescan_) { | |
549 self.rescanSoon(); | |
550 self.pendingRescan_ = false; | |
551 return true; | |
552 } | |
553 return false; | |
554 }; | |
555 | |
556 var onSuccess = function() { | |
557 self.runningScan_ = null; | |
558 successCallback(); | |
559 self.scanFailures_ = 0; | |
560 maybeRunPendingRescan(); | |
561 }; | |
562 | |
563 var onFailure = function() { | |
564 self.runningScan_ = null; | |
565 self.scanFailures_++; | |
566 failureCallback(); | |
567 | |
568 if (maybeRunPendingRescan()) | |
569 return; | |
570 | |
571 if (self.scanFailures_ <= 1) | |
572 self.rescanLater(); | |
573 }; | |
574 | |
575 this.runningScan_ = dirContents; | |
576 | |
577 dirContents.addEventListener('scan-completed', onSuccess); | |
578 dirContents.addEventListener('scan-updated', updatedCallback); | |
579 dirContents.addEventListener('scan-failed', onFailure); | |
580 dirContents.addEventListener('scan-cancelled', cancelledCallback); | |
581 dirContents.scan(); | |
582 }; | |
583 | |
584 /** | |
585 * @param {DirectoryContents} dirContents DirectoryContents instance. | |
586 * @private | |
587 */ | |
588 DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) { | |
589 cr.dispatchSimpleEvent(this, 'begin-update-files'); | |
590 this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() { | |
591 var selectedPaths = this.getSelectedPaths_(); | |
592 var selectedIndices = this.fileListSelection_.selectedIndexes; | |
593 | |
594 // Restore leadIndex in case leadName no longer exists. | |
595 var leadIndex = this.fileListSelection_.leadIndex; | |
596 var leadPath = this.getLeadPath_(); | |
597 | |
598 this.currentDirContents_ = dirContents; | |
599 dirContents.replaceContextFileList(); | |
600 | |
601 this.setSelectedPaths_(selectedPaths); | |
602 this.fileListSelection_.leadIndex = leadIndex; | |
603 this.setLeadPath_(leadPath); | |
604 | |
605 // If nothing is selected after update, then select file next to the | |
606 // latest selection | |
607 var forceChangeEvent = false; | |
608 if (this.fileListSelection_.selectedIndexes.length == 0 && | |
609 selectedIndices.length != 0) { | |
610 var maxIdx = Math.max.apply(null, selectedIndices); | |
611 this.selectIndex(Math.min(maxIdx - selectedIndices.length + 2, | |
612 this.getFileList().length) - 1); | |
613 forceChangeEvent = true; | |
614 } | |
615 return forceChangeEvent; | |
616 }.bind(this)); | |
617 | |
618 cr.dispatchSimpleEvent(this, 'end-update-files'); | |
619 }; | |
620 | |
621 /** | |
622 * Callback when an entry is changed. | |
623 * @param {util.EntryChangedKind} kind How the entry is changed. | |
624 * @param {Entry} entry The changed entry. | |
625 */ | |
626 DirectoryModel.prototype.onEntryChanged = function(kind, entry) { | |
627 // TODO(hidehiko): We should update directory model even the search result | |
628 // is shown. | |
629 var rootType = this.getCurrentRootType(); | |
630 if ((rootType === RootType.DRIVE || | |
631 rootType === RootType.DRIVE_SHARED_WITH_ME || | |
632 rootType === RootType.DRIVE_RECENT || | |
633 rootType === RootType.DRIVE_OFFLINE) && | |
634 this.isSearching()) | |
635 return; | |
636 | |
637 if (kind == util.EntryChangedKind.CREATED) { | |
638 entry.getParent(function(parentEntry) { | |
639 if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) { | |
640 // Do nothing if current directory changed during async operations. | |
641 return; | |
642 } | |
643 this.currentDirContents_.prefetchMetadata([entry], function() { | |
644 if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) { | |
645 // Do nothing if current directory changed during async operations. | |
646 return; | |
647 } | |
648 | |
649 var index = this.findIndexByEntry_(entry); | |
650 if (index >= 0) | |
651 this.getFileList().splice(index, 1, entry); | |
652 else | |
653 this.getFileList().push(entry); | |
654 }.bind(this)); | |
655 }.bind(this)); | |
656 } else { | |
657 // This is the delete event. | |
658 var index = this.findIndexByEntry_(entry); | |
659 if (index >= 0) | |
660 this.getFileList().splice(index, 1); | |
661 } | |
662 }; | |
663 | |
664 /** | |
665 * @param {Entry} entry The entry to be searched. | |
666 * @return {number} The index in the fileList, or -1 if not found. | |
667 * @private | |
668 */ | |
669 DirectoryModel.prototype.findIndexByEntry_ = function(entry) { | |
670 var fileList = this.getFileList(); | |
671 for (var i = 0; i < fileList.length; i++) { | |
672 if (util.isSameEntry(fileList.item(i), entry)) | |
673 return i; | |
674 } | |
675 return -1; | |
676 }; | |
677 | |
678 /** | |
679 * Called when rename is done successfully. | |
680 * Note: conceptually, DirectoryModel should work without this, because entries | |
681 * can be renamed by other systems anytime and Files.app should reflect it | |
682 * correctly. | |
683 * TODO(hidehiko): investigate more background, and remove this if possible. | |
684 * | |
685 * @param {Entry} oldEntry The old entry. | |
686 * @param {Entry} newEntry The new entry. | |
687 * @param {function()} opt_callback Called on completion. | |
688 */ | |
689 DirectoryModel.prototype.onRenameEntry = function( | |
690 oldEntry, newEntry, opt_callback) { | |
691 this.currentDirContents_.prefetchMetadata([newEntry], function() { | |
692 // If the current directory is the old entry, then quietly change to the | |
693 // new one. | |
694 if (util.isSameEntry(oldEntry, this.getCurrentDirEntry())) | |
695 this.changeDirectory(newEntry.fullPath); | |
696 | |
697 // Look for the old entry. | |
698 // If the entry doesn't exist in the list, it has been updated from | |
699 // outside (probably by directory rescan). | |
700 var index = this.findIndexByEntry_(oldEntry); | |
701 if (index >= 0) { | |
702 // Update the content list and selection status. | |
703 var wasSelected = this.fileListSelection_.getIndexSelected(index); | |
704 this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() { | |
705 this.fileListSelection_.setIndexSelected(index, false); | |
706 this.getFileList().splice(index, 1, newEntry); | |
707 if (wasSelected) { | |
708 // We re-search the index, because splice may trigger sorting so that | |
709 // index may be stale. | |
710 this.fileListSelection_.setIndexSelected( | |
711 this.findIndexByEntry_(newEntry), true); | |
712 } | |
713 return true; | |
714 }.bind(this)); | |
715 } | |
716 | |
717 // Run callback, finally. | |
718 if (opt_callback) | |
719 opt_callback(); | |
720 }.bind(this)); | |
721 }; | |
722 | |
723 /** | |
724 * Creates directory and updates the file list. | |
725 * | |
726 * @param {string} name Directory name. | |
727 * @param {function(DirectoryEntry)} successCallback Callback on success. | |
728 * @param {function(FileError)} errorCallback Callback on failure. | |
729 */ | |
730 DirectoryModel.prototype.createDirectory = function(name, successCallback, | |
731 errorCallback) { | |
732 var entry = this.getCurrentDirEntry(); | |
733 if (!entry) { | |
734 errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR)); | |
735 return; | |
736 } | |
737 | |
738 var onSuccess = function(newEntry) { | |
739 // Do not change anything or call the callback if current | |
740 // directory changed. | |
741 if (entry.fullPath != this.getCurrentDirPath()) | |
742 return; | |
743 | |
744 var existing = this.getFileList().slice().filter( | |
745 function(e) {return e.name == name;}); | |
746 | |
747 if (existing.length) { | |
748 this.selectEntry(name); | |
749 successCallback(existing[0]); | |
750 } else { | |
751 this.fileListSelection_.beginChange(); | |
752 this.getFileList().splice(0, 0, newEntry); | |
753 this.selectEntry(name); | |
754 this.fileListSelection_.endChange(); | |
755 successCallback(newEntry); | |
756 } | |
757 }; | |
758 | |
759 this.currentDirContents_.createDirectory(name, onSuccess.bind(this), | |
760 errorCallback); | |
761 }; | |
762 | |
763 /** | |
764 * Changes directory. Causes 'directory-change' event. | |
765 * | |
766 * @param {string} path New current directory path. | |
767 * @param {function(FileError)=} opt_errorCallback Executed if the change | |
768 * directory failed. | |
769 */ | |
770 DirectoryModel.prototype.changeDirectory = function(path, opt_errorCallback) { | |
771 if (PathUtil.isSpecialSearchRoot(path)) { | |
772 this.specialSearch(path, ''); | |
773 return; | |
774 } | |
775 | |
776 this.resolveDirectory(path, function(directoryEntry) { | |
777 this.changeDirectoryEntry_(directoryEntry); | |
778 }.bind(this), function(error) { | |
779 console.error('Error changing directory to ' + path + ': ', error); | |
780 if (opt_errorCallback) | |
781 opt_errorCallback(error); | |
782 }); | |
783 }; | |
784 | |
785 /** | |
786 * Resolves absolute directory path. Handles Drive stub. If the drive is | |
787 * mounting, callbacks will be called after the mount is completed. | |
788 * | |
789 * @param {string} path Path to the directory. | |
790 * @param {function(DirectoryEntry)} successCallback Success callback. | |
791 * @param {function(FileError)} errorCallback Error callback. | |
792 */ | |
793 DirectoryModel.prototype.resolveDirectory = function( | |
794 path, successCallback, errorCallback) { | |
795 if (PathUtil.getRootType(path) == RootType.DRIVE) { | |
796 if (!this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) { | |
797 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR)); | |
798 return; | |
799 } | |
800 } | |
801 | |
802 var onError = function(error) { | |
803 // Handle the special case, when in offline mode, and there are no cached | |
804 // contents on the C++ side. In such case, let's display the stub. | |
805 // The INVALID_STATE_ERR error code is returned from the drive filesystem | |
806 // in such situation. | |
807 // | |
808 // TODO(mtomasz, hashimoto): Consider rewriting this logic. | |
809 // crbug.com/253464. | |
810 if (PathUtil.getRootType(path) == RootType.DRIVE && | |
811 error.code == FileError.INVALID_STATE_ERR) { | |
812 successCallback(DirectoryModel.fakeDriveEntry_); | |
813 return; | |
814 } | |
815 errorCallback(error); | |
816 }.bind(this); | |
817 | |
818 this.volumeManager_.resolvePath( | |
819 path, | |
820 function(entry) { | |
821 if (entry.isFile) { | |
822 onError(util.createFileError(FileError.TYPE_MISMATCH_ERR)); | |
823 return; | |
824 } | |
825 successCallback(entry); | |
826 }, | |
827 onError); | |
828 }; | |
829 | |
830 /** | |
831 * @param {DirectoryEntry} dirEntry The absolute path to the new directory. | |
832 * @param {function()=} opt_callback Executed if the directory loads | |
833 * successfully. | |
834 * @private | |
835 */ | |
836 DirectoryModel.prototype.changeDirectoryEntrySilent_ = function(dirEntry, | |
837 opt_callback) { | |
838 var onScanComplete = function() { | |
839 if (opt_callback) | |
840 opt_callback(); | |
841 // For tests that open the dialog to empty directories, everything | |
842 // is loaded at this point. | |
843 chrome.test.sendMessage('directory-change-complete'); | |
844 }; | |
845 this.clearAndScan_( | |
846 DirectoryContents.createForDirectory(this.currentFileListContext_, | |
847 dirEntry), | |
848 onScanComplete.bind(this)); | |
849 }; | |
850 | |
851 /** | |
852 * Change the current directory to the directory represented by a | |
853 * DirectoryEntry. | |
854 * | |
855 * Dispatches the 'directory-changed' event when the directory is successfully | |
856 * changed. | |
857 * | |
858 * @param {DirectoryEntry} dirEntry The absolute path to the new directory. | |
859 * @param {function()=} opt_callback Executed if the directory loads | |
860 * successfully. | |
861 * @private | |
862 */ | |
863 DirectoryModel.prototype.changeDirectoryEntry_ = function( | |
864 dirEntry, opt_callback) { | |
865 this.fileWatcher_.changeWatchedDirectory(dirEntry, function() { | |
866 var previous = this.currentDirContents_.getDirectoryEntry(); | |
867 this.clearSearch_(); | |
868 this.changeDirectoryEntrySilent_(dirEntry, opt_callback); | |
869 | |
870 var e = new Event('directory-changed'); | |
871 e.previousDirEntry = previous; | |
872 e.newDirEntry = dirEntry; | |
873 this.dispatchEvent(e); | |
874 }.bind(this)); | |
875 }; | |
876 | |
877 /** | |
878 * Creates an object which could say whether directory has changed while it has | |
879 * been active or not. Designed for long operations that should be cancelled | |
880 * if the used change current directory. | |
881 * @return {Object} Created object. | |
882 */ | |
883 DirectoryModel.prototype.createDirectoryChangeTracker = function() { | |
884 var tracker = { | |
885 dm_: this, | |
886 active_: false, | |
887 hasChanged: false, | |
888 | |
889 start: function() { | |
890 if (!this.active_) { | |
891 this.dm_.addEventListener('directory-changed', | |
892 this.onDirectoryChange_); | |
893 this.active_ = true; | |
894 this.hasChanged = false; | |
895 } | |
896 }, | |
897 | |
898 stop: function() { | |
899 if (this.active_) { | |
900 this.dm_.removeEventListener('directory-changed', | |
901 this.onDirectoryChange_); | |
902 this.active_ = false; | |
903 } | |
904 }, | |
905 | |
906 onDirectoryChange_: function(event) { | |
907 tracker.stop(); | |
908 tracker.hasChanged = true; | |
909 } | |
910 }; | |
911 return tracker; | |
912 }; | |
913 | |
914 /** | |
915 * Change the state of the model to reflect the specified path (either a | |
916 * file or directory). | |
917 * TODO(hidehiko): This logic should be merged with | |
918 * FileManager.setupCurrentDirectory_. | |
919 * | |
920 * @param {string} path The root path to use. | |
921 * @param {function(string, string, boolean)=} opt_pathResolveCallback Invoked | |
922 * as soon as the path has been resolved, and called with the base and leaf | |
923 * portions of the path name, and a flag indicating if the entry exists. | |
924 * Will be called even if another directory change happened while setupPath | |
925 * was in progress, but will pass |false| as |exist| parameter. | |
926 */ | |
927 DirectoryModel.prototype.setupPath = function(path, opt_pathResolveCallback) { | |
928 var tracker = this.createDirectoryChangeTracker(); | |
929 tracker.start(); | |
930 | |
931 var self = this; | |
932 var resolveCallback = function(directoryPath, fileName, exists) { | |
933 tracker.stop(); | |
934 if (!opt_pathResolveCallback) | |
935 return; | |
936 opt_pathResolveCallback(directoryPath, fileName, | |
937 exists && !tracker.hasChanged); | |
938 }; | |
939 | |
940 var changeDirectoryEntry = function(directoryEntry, opt_callback) { | |
941 tracker.stop(); | |
942 if (!tracker.hasChanged) | |
943 self.changeDirectoryEntry_(directoryEntry, opt_callback); | |
944 }; | |
945 | |
946 var EXISTS = true; | |
947 | |
948 var changeToDefault = function(leafName) { | |
949 var def = PathUtil.DEFAULT_DIRECTORY; | |
950 self.resolveDirectory(def, function(directoryEntry) { | |
951 resolveCallback(def, leafName, !EXISTS); | |
952 changeDirectoryEntry(directoryEntry); | |
953 }, function(error) { | |
954 console.error('Failed to resolve default directory: ' + def, error); | |
955 resolveCallback('/', leafName, !EXISTS); | |
956 }); | |
957 }; | |
958 | |
959 var noParentDirectory = function(leafName, error) { | |
960 console.warn('Can\'t resolve parent directory: ' + path, error); | |
961 changeToDefault(leafName); | |
962 }; | |
963 | |
964 if (DirectoryModel.isSystemDirectory(path)) { | |
965 changeToDefault(''); | |
966 return; | |
967 } | |
968 | |
969 this.resolveDirectory(path, function(directoryEntry) { | |
970 resolveCallback(directoryEntry.fullPath, '', !EXISTS); | |
971 changeDirectoryEntry(directoryEntry); | |
972 }, function(error) { | |
973 // Usually, leaf does not exist, because it's just a suggested file name. | |
974 var fileExists = error.code == FileError.TYPE_MISMATCH_ERR; | |
975 var nameDelimiter = path.lastIndexOf('/'); | |
976 var parentDirectoryPath = path.substr(0, nameDelimiter); | |
977 var leafName = path.substr(nameDelimiter + 1); | |
978 if (fileExists || error.code == FileError.NOT_FOUND_ERR) { | |
979 if (DirectoryModel.isSystemDirectory(parentDirectoryPath)) { | |
980 changeToDefault(leafName); | |
981 return; | |
982 } | |
983 self.resolveDirectory(parentDirectoryPath, | |
984 function(parentDirectoryEntry) { | |
985 var fileName = path.substr(nameDelimiter + 1); | |
986 resolveCallback(parentDirectoryEntry.fullPath, fileName, fileExists); | |
987 changeDirectoryEntry(parentDirectoryEntry, | |
988 function() { | |
989 self.selectEntry(fileName); | |
990 }); | |
991 }, noParentDirectory.bind(null, leafName)); | |
992 } else { | |
993 // Unexpected errors. | |
994 console.error('Directory resolving error: ', error); | |
995 changeToDefault(leafName); | |
996 } | |
997 }); | |
998 }; | |
999 | |
1000 /** | |
1001 * @param {string} name Filename. | |
1002 */ | |
1003 DirectoryModel.prototype.selectEntry = function(name) { | |
1004 var fileList = this.getFileList(); | |
1005 for (var i = 0; i < fileList.length; i++) { | |
1006 if (fileList.item(i).name == name) { | |
1007 this.selectIndex(i); | |
1008 return; | |
1009 } | |
1010 } | |
1011 }; | |
1012 | |
1013 /** | |
1014 * @param {Array.<string>} urls Array of URLs. | |
1015 */ | |
1016 DirectoryModel.prototype.selectUrls = function(urls) { | |
1017 var fileList = this.getFileList(); | |
1018 this.fileListSelection_.beginChange(); | |
1019 this.fileListSelection_.unselectAll(); | |
1020 for (var i = 0; i < fileList.length; i++) { | |
1021 if (urls.indexOf(fileList.item(i).toURL()) >= 0) | |
1022 this.fileListSelection_.setIndexSelected(i, true); | |
1023 } | |
1024 this.fileListSelection_.endChange(); | |
1025 }; | |
1026 | |
1027 /** | |
1028 * @param {number} index Index of file. | |
1029 */ | |
1030 DirectoryModel.prototype.selectIndex = function(index) { | |
1031 // this.focusCurrentList_(); | |
1032 if (index >= this.getFileList().length) | |
1033 return; | |
1034 | |
1035 // If a list bound with the model it will do scrollIndexIntoView(index). | |
1036 this.fileListSelection_.selectedIndex = index; | |
1037 }; | |
1038 | |
1039 /** | |
1040 * Called when VolumeInfoList is updated. | |
1041 * | |
1042 * @param {Event} event Event of VolumeInfoList's 'splice'. | |
1043 * @private | |
1044 */ | |
1045 DirectoryModel.prototype.onVolumeInfoListUpdated_ = function(event) { | |
1046 var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); | |
1047 if (driveVolume && !driveVolume.error) { | |
1048 var currentDirEntry = this.getCurrentDirEntry(); | |
1049 if (currentDirEntry) { | |
1050 if (currentDirEntry === DirectoryModel.fakeDriveEntry_) { | |
1051 // Replace the fake entry by real DirectoryEntry silently. | |
1052 this.volumeManager_.resolvePath( | |
1053 DirectoryModel.fakeDriveEntry_.fullPath, | |
1054 function(entry) { | |
1055 // If the current entry is still fake drive entry, replace it. | |
1056 if (this.getCurrentDirEntry() === DirectoryModel.fakeDriveEntry_) | |
1057 this.changeDirectoryEntrySilent_(entry); | |
1058 }, | |
1059 function(error) {}); | |
1060 } else if (PathUtil.isSpecialSearchRoot(currentDirEntry.fullPath)) { | |
1061 for (var i = 0; i < event.added.length; i++) { | |
1062 if (event.added[i].volumeType == util.VolumeType.DRIVE) { | |
1063 // If the Drive volume is newly mounted, rescan it. | |
1064 this.rescan(); | |
1065 break; | |
1066 } | |
1067 } | |
1068 } | |
1069 } | |
1070 } | |
1071 | |
1072 var rootPath = this.getCurrentRootPath(); | |
1073 var rootType = PathUtil.getRootType(rootPath); | |
1074 | |
1075 // If the path is on drive, reduce to the Drive's mount point. | |
1076 if (rootType === RootType.DRIVE) | |
1077 rootPath = RootDirectory.DRIVE; | |
1078 | |
1079 // When the volume where we are is unmounted, fallback to | |
1080 // DEFAULT_DIRECTORY. | |
1081 // Note: during the initialization, rootType can be undefined. | |
1082 if (rootType && !this.volumeManager_.getVolumeInfo(rootPath)) | |
1083 this.changeDirectory(PathUtil.DEFAULT_DIRECTORY); | |
1084 }; | |
1085 | |
1086 /** | |
1087 * @param {string} path Path. | |
1088 * @return {boolean} If current directory is system. | |
1089 */ | |
1090 DirectoryModel.isSystemDirectory = function(path) { | |
1091 path = path.replace(/\/+$/, ''); | |
1092 return path === RootDirectory.REMOVABLE || path === RootDirectory.ARCHIVE; | |
1093 }; | |
1094 | |
1095 /** | |
1096 * Check if the root of the given path is mountable or not. | |
1097 * | |
1098 * @param {string} path Path. | |
1099 * @return {boolean} Return true, if the given path is under mountable root. | |
1100 * Otherwise, return false. | |
1101 */ | |
1102 DirectoryModel.isMountableRoot = function(path) { | |
1103 var rootType = PathUtil.getRootType(path); | |
1104 switch (rootType) { | |
1105 case RootType.DOWNLOADS: | |
1106 return false; | |
1107 case RootType.ARCHIVE: | |
1108 case RootType.REMOVABLE: | |
1109 case RootType.DRIVE: | |
1110 return true; | |
1111 default: | |
1112 throw new Error('Unknown root type!'); | |
1113 } | |
1114 }; | |
1115 | |
1116 /** | |
1117 * Performs search and displays results. The search type is dependent on the | |
1118 * current directory. If we are currently on drive, server side content search | |
1119 * over drive mount point. If the current directory is not on the drive, file | |
1120 * name search over current directory will be performed. | |
1121 * | |
1122 * @param {string} query Query that will be searched for. | |
1123 * @param {function(Event)} onSearchRescan Function that will be called when the | |
1124 * search directory is rescanned (i.e. search results are displayed). | |
1125 * @param {function()} onClearSearch Function to be called when search state | |
1126 * gets cleared. | |
1127 * TODO(olege): Change callbacks to events. | |
1128 */ | |
1129 DirectoryModel.prototype.search = function(query, | |
1130 onSearchRescan, | |
1131 onClearSearch) { | |
1132 query = query.trimLeft(); | |
1133 | |
1134 this.clearSearch_(); | |
1135 | |
1136 var currentDirEntry = this.getCurrentDirEntry(); | |
1137 if (!currentDirEntry) { | |
1138 // Not yet initialized. Do nothing. | |
1139 return; | |
1140 } | |
1141 | |
1142 if (!query) { | |
1143 if (this.isSearching()) { | |
1144 var newDirContents = DirectoryContents.createForDirectory( | |
1145 this.currentFileListContext_, | |
1146 this.currentDirContents_.getLastNonSearchDirectoryEntry()); | |
1147 this.clearAndScan_(newDirContents); | |
1148 } | |
1149 return; | |
1150 } | |
1151 | |
1152 this.onSearchCompleted_ = onSearchRescan; | |
1153 this.onClearSearch_ = onClearSearch; | |
1154 | |
1155 this.addEventListener('scan-completed', this.onSearchCompleted_); | |
1156 | |
1157 // If we are offline, let's fallback to file name search inside dir. | |
1158 // A search initiated from directories in Drive or special search results | |
1159 // should trigger Drive search. | |
1160 var newDirContents; | |
1161 if (!this.isDriveOffline() && | |
1162 PathUtil.isDriveBasedPath(currentDirEntry.fullPath)) { | |
1163 // Drive search is performed over the whole drive, so pass drive root as | |
1164 // |directoryEntry|. | |
1165 newDirContents = DirectoryContents.createForDriveSearch( | |
1166 this.currentFileListContext_, | |
1167 currentDirEntry, | |
1168 this.currentDirContents_.getLastNonSearchDirectoryEntry(), | |
1169 query); | |
1170 } else { | |
1171 newDirContents = DirectoryContents.createForLocalSearch( | |
1172 this.currentFileListContext_, currentDirEntry, query); | |
1173 } | |
1174 this.clearAndScan_(newDirContents); | |
1175 }; | |
1176 | |
1177 /** | |
1178 * Performs special search and displays results. e.g. Drive files available | |
1179 * offline, shared-with-me files, recently modified files. | |
1180 * @param {string} path Path string representing special search. See fake | |
1181 * entries in PathUtil.RootDirectory. | |
1182 * @param {string=} opt_query Query string used for the search. | |
1183 */ | |
1184 DirectoryModel.prototype.specialSearch = function(path, opt_query) { | |
1185 var query = opt_query || ''; | |
1186 | |
1187 this.clearSearch_(); | |
1188 | |
1189 this.onSearchCompleted_ = null; | |
1190 this.onClearSearch_ = null; | |
1191 | |
1192 var onDriveDirectoryResolved = function(driveRoot) { | |
1193 if (!driveRoot || driveRoot == DirectoryModel.fakeDriveEntry_) { | |
1194 // Drive root not available or not ready. onVolumeInfoListUpdated_() | |
1195 // handles the rescan if necessary. | |
1196 driveRoot = null; | |
1197 } | |
1198 | |
1199 var specialSearchType = PathUtil.getRootType(path); | |
1200 var searchOption; | |
1201 var dirEntry; | |
1202 if (specialSearchType == RootType.DRIVE_OFFLINE) { | |
1203 dirEntry = DirectoryModel.fakeDriveOfflineEntry_; | |
1204 searchOption = | |
1205 DriveMetadataSearchContentScanner.SearchType.SEARCH_OFFLINE; | |
1206 } else if (specialSearchType == RootType.DRIVE_SHARED_WITH_ME) { | |
1207 dirEntry = DirectoryModel.fakeDriveSharedWithMeEntry_; | |
1208 searchOption = | |
1209 DriveMetadataSearchContentScanner.SearchType.SEARCH_SHARED_WITH_ME; | |
1210 } else if (specialSearchType == RootType.DRIVE_RECENT) { | |
1211 dirEntry = DirectoryModel.fakeDriveRecentEntry_; | |
1212 searchOption = | |
1213 DriveMetadataSearchContentScanner.SearchType.SEARCH_RECENT_FILES; | |
1214 | |
1215 } else { | |
1216 // Unknown path. | |
1217 this.changeDirectory(PathUtil.DEFAULT_DIRECTORY); | |
1218 return; | |
1219 } | |
1220 | |
1221 var newDirContents = DirectoryContents.createForDriveMetadataSearch( | |
1222 this.currentFileListContext_, | |
1223 dirEntry, driveRoot, query, searchOption); | |
1224 var previous = this.currentDirContents_.getDirectoryEntry(); | |
1225 this.clearAndScan_(newDirContents); | |
1226 | |
1227 var e = new Event('directory-changed'); | |
1228 e.previousDirEntry = previous; | |
1229 e.newDirEntry = dirEntry; | |
1230 this.dispatchEvent(e); | |
1231 }.bind(this); | |
1232 | |
1233 this.resolveDirectory(DirectoryModel.fakeDriveEntry_.fullPath, | |
1234 onDriveDirectoryResolved /* success */, | |
1235 function() {} /* failed */); | |
1236 }; | |
1237 | |
1238 /** | |
1239 * In case the search was active, remove listeners and send notifications on | |
1240 * its canceling. | |
1241 * @private | |
1242 */ | |
1243 DirectoryModel.prototype.clearSearch_ = function() { | |
1244 if (!this.isSearching()) | |
1245 return; | |
1246 | |
1247 if (this.onSearchCompleted_) { | |
1248 this.removeEventListener('scan-completed', this.onSearchCompleted_); | |
1249 this.onSearchCompleted_ = null; | |
1250 } | |
1251 | |
1252 if (this.onClearSearch_) { | |
1253 this.onClearSearch_(); | |
1254 this.onClearSearch_ = null; | |
1255 } | |
1256 }; | |
OLD | NEW |