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 /** | |
8 * Entry of NavigationListModel. This constructor should be called only from | |
9 * the helper methods (NavigationModelItem.createFromPath/createFromEntry). | |
10 * | |
11 * @param {string} path Path. | |
12 * @param {DirectoryEntry} entry Entry. Can be null. | |
13 * @constructor | |
14 */ | |
15 function NavigationModelItem(path, entry) { | |
16 this.path_ = path; | |
17 this.entry_ = entry; | |
18 this.resolvingQueue_ = new AsyncUtil.Queue(); | |
19 | |
20 Object.seal(this); | |
21 } | |
22 | |
23 NavigationModelItem.prototype = { | |
24 get path() { return this.path_; }, | |
25 }; | |
26 | |
27 /** | |
28 * Returns the cached entry of the item. This may return NULL if the target is | |
29 * not available on the filesystem, is not resolved or is under resolving the | |
30 * entry. | |
31 * | |
32 * @return {Entry} Cached entry. | |
33 */ | |
34 NavigationModelItem.prototype.getCachedEntry = function() { | |
35 return this.entry_; | |
36 }; | |
37 | |
38 /** | |
39 * @param {VolumeManagerWrapper} volumeManager VolumeManagerWrapper instance. | |
40 * @param {string} path Path. | |
41 * @param {function(FileError)} errorCallback Called when the resolving is | |
42 * failed with the error. | |
43 * @return {NavigationModelItem} Created NavigationModelItem. | |
44 */ | |
45 NavigationModelItem.createFromPath = function( | |
46 volumeManager, path, errorCallback) { | |
47 var item = new NavigationModelItem(path, null); | |
48 item.resolvingQueue_.run(function(continueCallback) { | |
49 volumeManager.resolvePath( | |
50 path, | |
51 function(entry) { | |
52 if (entry.isDirectory) | |
53 item.entry_ = entry; | |
54 else | |
55 errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR)); | |
56 continueCallback(); | |
57 }, | |
58 function(error) { | |
59 errorCallback(error); | |
60 continueCallback(); | |
61 }); | |
62 }); | |
63 return item; | |
64 }; | |
65 | |
66 /** | |
67 * @param {DirectoryEntry} entry Entry. Can be null. | |
68 * @return {NavigationModelItem} Created NavigationModelItem. | |
69 */ | |
70 NavigationModelItem.createFromEntry = function(entry) { | |
71 if (!entry) | |
72 return null; | |
73 return new NavigationModelItem(entry.fullPath, entry); | |
74 }; | |
75 | |
76 /** | |
77 * Retrieves the entry. If the entry is being retrieved, waits until it | |
78 * finishes. | |
79 * @param {function(Entry)} callback Called with the resolved entry. The entry | |
80 * may be NULL if resolving is failed. | |
81 */ | |
82 NavigationModelItem.prototype.getEntryAsync = function(callback) { | |
83 // If resolving the entry is running, wait until it finishes. | |
84 this.resolvingQueue_.run(function(continueCallback) { | |
85 callback(this.entry_); | |
86 continueCallback(); | |
87 }.bind(this)); | |
88 }; | |
89 | |
90 /** | |
91 * Returns if this item is a shortcut or a volume root. | |
92 * @return {boolean} True if a shortcut, false if a volume root. | |
93 */ | |
94 NavigationModelItem.prototype.isShortcut = function() { | |
95 return !PathUtil.isRootPath(this.path_); | |
96 }; | |
97 | |
98 /** | |
99 * A navigation list model. This model combines the 2 lists. | |
100 * @param {VolumeManagerWrapper} volumeManager VolumeManagerWrapper instance. | |
101 * @param {cr.ui.ArrayDataModel} shortcutListModel The list of folder shortcut. | |
102 * @constructor | |
103 * @extends {cr.EventTarget} | |
104 */ | |
105 function NavigationListModel(volumeManager, shortcutListModel) { | |
106 cr.EventTarget.call(this); | |
107 | |
108 this.volumeManager_ = volumeManager; | |
109 this.shortcutListModel_ = shortcutListModel; | |
110 | |
111 var volumeInfoToModelItem = function(volumeInfo) { | |
112 if (volumeInfo.volumeType == util.VolumeType.DRIVE) { | |
113 // For drive volume, we assign the path to "My Drive". | |
114 return NavigationModelItem.createFromPath( | |
115 this.volumeManager_, | |
116 volumeInfo.mountPath + '/root', | |
117 function() {}); | |
118 } else { | |
119 return NavigationModelItem.createFromEntry(volumeInfo.root); | |
120 } | |
121 }.bind(this); | |
122 | |
123 var pathToModelItem = function(path) { | |
124 var item = NavigationModelItem.createFromPath( | |
125 this.volumeManager_, | |
126 path, | |
127 function(error) { | |
128 if (error.code == FileError.NOT_FOUND_ERR) | |
129 this.onItemNotFoundError(item); | |
130 }.bind(this)); | |
131 return item; | |
132 }.bind(this); | |
133 | |
134 /** | |
135 * Type of updated list. | |
136 * @enum {number} | |
137 * @const | |
138 */ | |
139 var ListType = { | |
140 VOLUME_LIST: 1, | |
141 SHORTCUT_LIST: 2 | |
142 }; | |
143 Object.freeze(ListType); | |
144 | |
145 // Generates this.volumeList_ and this.shortcutList_ from the models. | |
146 this.volumeList_ = | |
147 this.volumeManager_.volumeInfoList.slice().map(volumeInfoToModelItem); | |
148 | |
149 this.shortcutList_ = []; | |
150 for (var i = 0; i < this.shortcutListModel_.length; i++) { | |
151 var shortcutPath = this.shortcutListModel_.item(i); | |
152 var mountPath = PathUtil.isDriveBasedPath(shortcutPath) ? | |
153 RootDirectory.DRIVE : | |
154 PathUtil.getRootPath(shortcutPath); | |
155 var volumeInfo = this.volumeManager_.getVolumeInfo(mountPath); | |
156 var isMounted = volumeInfo && !volumeInfo.error; | |
157 if (isMounted) | |
158 this.shortcutList_.push(pathToModelItem(shortcutPath)); | |
159 } | |
160 | |
161 // Generates a combined 'permuted' event from an event of either list. | |
162 var permutedHandler = function(listType, event) { | |
163 var permutation; | |
164 | |
165 // Build the volumeList. | |
166 if (listType == ListType.VOLUME_LIST) { | |
167 // The volume is mounted or unmounted. | |
168 var newList = []; | |
169 | |
170 // Use the old instances if they just move. | |
171 for (var i = 0; i < event.permutation.length; i++) { | |
172 if (event.permutation[i] >= 0) | |
173 newList[event.permutation[i]] = this.volumeList_[i]; | |
174 } | |
175 | |
176 // Create missing instances. | |
177 for (var i = 0; i < event.newLength; i++) { | |
178 if (!newList[i]) { | |
179 newList[i] = volumeInfoToModelItem( | |
180 this.volumeManager_.volumeInfoList.item(i)); | |
181 } | |
182 } | |
183 this.volumeList_ = newList; | |
184 | |
185 permutation = event.permutation.slice(); | |
186 } else { | |
187 // volumeList part has not been changed, so the permutation should be | |
188 // idenetity mapping. | |
189 permutation = []; | |
190 for (var i = 0; i < this.volumeList_.length; i++) | |
191 permutation[i] = i; | |
192 } | |
193 | |
194 // Build the shortcutList. Even if the event is for the volumeInfoList | |
195 // update, the short cut path may be unmounted or newly mounted. So, here | |
196 // shortcutList will always be re-built. | |
197 // Currently this code may be redundant, as shortcut folder is supported | |
198 // only on Drive File System and we can assume single-profile, but | |
199 // multi-profile will be supported later. | |
200 // The shortcut list is sorted in case-insensitive lexicographical order. | |
201 // So we just can traverse the two list linearly. | |
202 var modelIndex = 0; | |
203 var oldListIndex = 0; | |
204 var newList = []; | |
205 while (modelIndex < this.shortcutListModel_.length && | |
206 oldListIndex < this.shortcutList_.length) { | |
207 var shortcutPath = this.shortcutListModel_.item(modelIndex); | |
208 var cmp = this.shortcutListModel_.compare( | |
209 shortcutPath, this.shortcutList_[oldListIndex].path); | |
210 if (cmp > 0) { | |
211 // The shortcut at shortcutList_[oldListIndex] is removed. | |
212 permutation.push(-1); | |
213 oldListIndex++; | |
214 continue; | |
215 } | |
216 | |
217 // Check if the volume where the shortcutPath is is mounted or not. | |
218 var mountPath = PathUtil.isDriveBasedPath(shortcutPath) ? | |
219 RootDirectory.DRIVE : | |
220 PathUtil.getRootPath(shortcutPath); | |
221 var volumeInfo = this.volumeManager_.getVolumeInfo(mountPath); | |
222 var isMounted = volumeInfo && !volumeInfo.error; | |
223 if (cmp == 0) { | |
224 // There exists an old NavigationModelItem instance. | |
225 if (isMounted) { | |
226 // Reuse the old instance. | |
227 permutation.push(newList.length + this.volumeList_.length); | |
228 newList.push(this.shortcutList_[oldListIndex]); | |
229 } else { | |
230 permutation.push(-1); | |
231 } | |
232 oldListIndex++; | |
233 } else { | |
234 // We needs to create a new instance for the shortcut path. | |
235 if (isMounted) | |
236 newList.push(pathToModelItem(shortcutPath)); | |
237 } | |
238 modelIndex++; | |
239 } | |
240 | |
241 // Add remaining (new) shortcuts if necessary. | |
242 for (; modelIndex < this.shortcutListModel_.length; modelIndex++) { | |
243 var shortcutPath = this.shortcutListModel_.item(modelIndex); | |
244 var mountPath = PathUtil.isDriveBasedPath(shortcutPath) ? | |
245 RootDirectory.DRIVE : | |
246 PathUtil.getRootPath(shortcutPath); | |
247 var volumeInfo = this.volumeManager_.getVolumeInfo(mountPath); | |
248 var isMounted = volumeInfo && !volumeInfo.error; | |
249 if (isMounted) | |
250 newList.push(pathToModelItem(shortcutPath)); | |
251 } | |
252 | |
253 // Fill remaining permutation if necessary. | |
254 for (; oldListIndex < this.shortcutList_.length; oldListIndex++) | |
255 permutation.push(-1); | |
256 | |
257 this.shortcutList_ = newList; | |
258 | |
259 // Dispatch permuted event. | |
260 var permutedEvent = new Event('permuted'); | |
261 permutedEvent.newLength = | |
262 this.volumeList_.length + this.shortcutList_.length; | |
263 permutedEvent.permutation = permutation; | |
264 this.dispatchEvent(permutedEvent); | |
265 }; | |
266 | |
267 this.volumeManager_.volumeInfoList.addEventListener( | |
268 'permuted', permutedHandler.bind(this, ListType.VOLUME_LIST)); | |
269 this.shortcutListModel_.addEventListener( | |
270 'permuted', permutedHandler.bind(this, ListType.SHORTCUT_LIST)); | |
271 | |
272 // 'change' event is just ignored, because it is not fired neither in | |
273 // the folder shortcut list nor in the volume info list. | |
274 // 'splice' and 'sorted' events are not implemented, since they are not used | |
275 // in list.js. | |
276 } | |
277 | |
278 /** | |
279 * NavigationList inherits cr.EventTarget. | |
280 */ | |
281 NavigationListModel.prototype = { | |
282 __proto__: cr.EventTarget.prototype, | |
283 get length() { return this.length_(); }, | |
284 get folderShortcutList() { return this.shortcutList_; } | |
285 }; | |
286 | |
287 /** | |
288 * Returns the item at the given index. | |
289 * @param {number} index The index of the entry to get. | |
290 * @return {?string} The path at the given index. | |
291 */ | |
292 NavigationListModel.prototype.item = function(index) { | |
293 var offset = this.volumeList_.length; | |
294 if (index < offset) | |
295 return this.volumeList_[index]; | |
296 return this.shortcutList_[index - offset]; | |
297 }; | |
298 | |
299 /** | |
300 * Returns the number of items in the model. | |
301 * @return {number} The length of the model. | |
302 * @private | |
303 */ | |
304 NavigationListModel.prototype.length_ = function() { | |
305 return this.volumeList_.length + this.shortcutList_.length; | |
306 }; | |
307 | |
308 /** | |
309 * Returns the first matching item. | |
310 * @param {NavigationModelItem} modelItem The entry to find. | |
311 * @param {number=} opt_fromIndex If provided, then the searching start at | |
312 * the {@code opt_fromIndex}. | |
313 * @return {number} The index of the first found element or -1 if not found. | |
314 */ | |
315 NavigationListModel.prototype.indexOf = function(modelItem, opt_fromIndex) { | |
316 for (var i = opt_fromIndex || 0; i < this.length; i++) { | |
317 if (modelItem === this.item(i)) | |
318 return i; | |
319 } | |
320 return -1; | |
321 }; | |
322 | |
323 /** | |
324 * Called when one od the items is not found on the filesystem. | |
325 * @param {NavigationModelItem} modelItem The entry which is not found. | |
326 */ | |
327 NavigationListModel.prototype.onItemNotFoundError = function(modelItem) { | |
328 var index = this.indexOf(modelItem); | |
329 if (index === -1) { | |
330 // Invalid modelItem. | |
331 } else if (index < this.volumeList_.length) { | |
332 // The item is in the volume list. | |
333 // Not implemented. | |
334 // TODO(yoshiki): Implement it when necessary. | |
335 } else { | |
336 // The item is in the folder shortcut list. | |
337 if (this.isDriveMounted()) | |
338 this.shortcutListModel_.remove(modelItem.path); | |
339 } | |
340 }; | |
341 | |
342 /** | |
343 * Returns if the drive is mounted or not. | |
344 * @return {boolean} True if the drive is mounted, false otherwise. | |
345 */ | |
346 NavigationListModel.prototype.isDriveMounted = function() { | |
347 return !!this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); | |
348 }; | |
349 | |
350 /** | |
351 * A navigation list item. | |
352 * @constructor | |
353 * @extends {HTMLLIElement} | |
354 */ | |
355 var NavigationListItem = cr.ui.define('li'); | |
356 | |
357 NavigationListItem.prototype = { | |
358 __proto__: HTMLLIElement.prototype, | |
359 get modelItem() { return this.modelItem_; } | |
360 }; | |
361 | |
362 /** | |
363 * Decorate the item. | |
364 */ | |
365 NavigationListItem.prototype.decorate = function() { | |
366 // decorate() may be called twice: from the constructor and from | |
367 // List.createItem(). This check prevents double-decorating. | |
368 if (this.className) | |
369 return; | |
370 | |
371 this.className = 'root-item'; | |
372 this.setAttribute('role', 'option'); | |
373 | |
374 this.iconDiv_ = cr.doc.createElement('div'); | |
375 this.iconDiv_.className = 'volume-icon'; | |
376 this.appendChild(this.iconDiv_); | |
377 | |
378 this.label_ = cr.doc.createElement('div'); | |
379 this.label_.className = 'root-label'; | |
380 this.appendChild(this.label_); | |
381 | |
382 cr.defineProperty(this, 'lead', cr.PropertyKind.BOOL_ATTR); | |
383 cr.defineProperty(this, 'selected', cr.PropertyKind.BOOL_ATTR); | |
384 }; | |
385 | |
386 /** | |
387 * Associate a path with this item. | |
388 * @param {NavigationModelItem} modelItem NavigationModelItem of this item. | |
389 * @param {string=} opt_deviceType The type of the device. Available iff the | |
390 * path represents removable storage. | |
391 */ | |
392 NavigationListItem.prototype.setModelItem = | |
393 function(modelItem, opt_deviceType) { | |
394 if (this.modelItem_) | |
395 console.warn('NavigationListItem.setModelItem should be called only once.'); | |
396 | |
397 this.modelItem_ = modelItem; | |
398 | |
399 var rootType = PathUtil.getRootType(modelItem.path); | |
400 this.iconDiv_.setAttribute('volume-type-icon', rootType); | |
401 if (opt_deviceType) { | |
402 this.iconDiv_.setAttribute('volume-subtype', opt_deviceType); | |
403 } | |
404 | |
405 this.label_.textContent = PathUtil.getFolderLabel(modelItem.path); | |
406 | |
407 if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) { | |
408 this.eject_ = cr.doc.createElement('div'); | |
409 // Block other mouse handlers. | |
410 this.eject_.addEventListener( | |
411 'mouseup', function(event) { event.stopPropagation() }); | |
412 this.eject_.addEventListener( | |
413 'mousedown', function(event) { event.stopPropagation() }); | |
414 | |
415 this.eject_.className = 'root-eject'; | |
416 this.eject_.addEventListener('click', function(event) { | |
417 event.stopPropagation(); | |
418 cr.dispatchSimpleEvent(this, 'eject'); | |
419 }.bind(this)); | |
420 | |
421 this.appendChild(this.eject_); | |
422 } | |
423 }; | |
424 | |
425 /** | |
426 * Associate a context menu with this item. | |
427 * @param {cr.ui.Menu} menu Menu this item. | |
428 */ | |
429 NavigationListItem.prototype.maybeSetContextMenu = function(menu) { | |
430 if (!this.modelItem_.path) { | |
431 console.error('NavigationListItem.maybeSetContextMenu must be called ' + | |
432 'after setModelItem().'); | |
433 return; | |
434 } | |
435 | |
436 var isRoot = PathUtil.isRootPath(this.modelItem_.path); | |
437 var rootType = PathUtil.getRootType(this.modelItem_.path); | |
438 // The context menu is shown on the following items: | |
439 // - Removable and Archive volumes | |
440 // - Folder shortcuts | |
441 if (!isRoot || | |
442 (rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS)) | |
443 cr.ui.contextMenuHandler.setContextMenu(this, menu); | |
444 }; | |
445 | |
446 /** | |
447 * A navigation list. | |
448 * @constructor | |
449 * @extends {cr.ui.List} | |
450 */ | |
451 function NavigationList() { | |
452 } | |
453 | |
454 /** | |
455 * NavigationList inherits cr.ui.List. | |
456 */ | |
457 NavigationList.prototype = { | |
458 __proto__: cr.ui.List.prototype, | |
459 | |
460 set dataModel(dataModel) { | |
461 if (!this.onListContentChangedBound_) | |
462 this.onListContentChangedBound_ = this.onListContentChanged_.bind(this); | |
463 | |
464 if (this.dataModel_) { | |
465 this.dataModel_.removeEventListener( | |
466 'change', this.onListContentChangedBound_); | |
467 this.dataModel_.removeEventListener( | |
468 'permuted', this.onListContentChangedBound_); | |
469 } | |
470 | |
471 var parentSetter = cr.ui.List.prototype.__lookupSetter__('dataModel'); | |
472 parentSetter.call(this, dataModel); | |
473 | |
474 // This must be placed after the parent method is called, in order to make | |
475 // it sure that the list was changed. | |
476 dataModel.addEventListener('change', this.onListContentChangedBound_); | |
477 dataModel.addEventListener('permuted', this.onListContentChangedBound_); | |
478 }, | |
479 | |
480 get dataModel() { | |
481 return this.dataModel_; | |
482 }, | |
483 | |
484 // TODO(yoshiki): Add a setter of 'directoryModel'. | |
485 }; | |
486 | |
487 /** | |
488 * @param {HTMLElement} el Element to be DirectoryItem. | |
489 * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system. | |
490 * @param {DirectoryModel} directoryModel Current DirectoryModel. | |
491 * folders. | |
492 */ | |
493 NavigationList.decorate = function(el, volumeManager, directoryModel) { | |
494 el.__proto__ = NavigationList.prototype; | |
495 el.decorate(volumeManager, directoryModel); | |
496 }; | |
497 | |
498 /** | |
499 * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system. | |
500 * @param {DirectoryModel} directoryModel Current DirectoryModel. | |
501 */ | |
502 NavigationList.prototype.decorate = function(volumeManager, directoryModel) { | |
503 cr.ui.List.decorate(this); | |
504 this.__proto__ = NavigationList.prototype; | |
505 | |
506 this.directoryModel_ = directoryModel; | |
507 this.volumeManager_ = volumeManager; | |
508 this.selectionModel = new cr.ui.ListSingleSelectionModel(); | |
509 | |
510 this.directoryModel_.addEventListener('directory-changed', | |
511 this.onCurrentDirectoryChanged_.bind(this)); | |
512 this.selectionModel.addEventListener( | |
513 'change', this.onSelectionChange_.bind(this)); | |
514 this.selectionModel.addEventListener( | |
515 'beforeChange', this.onBeforeSelectionChange_.bind(this)); | |
516 | |
517 this.scrollBar_ = new ScrollBar(); | |
518 this.scrollBar_.initialize(this.parentNode, this); | |
519 | |
520 // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox' | |
521 // role for better accessibility on ChromeOS. | |
522 this.setAttribute('role', 'listbox'); | |
523 | |
524 var self = this; | |
525 this.itemConstructor = function(modelItem) { | |
526 return self.renderRoot_(modelItem); | |
527 }; | |
528 }; | |
529 | |
530 /** | |
531 * This overrides cr.ui.List.measureItem(). | |
532 * In the method, a temporary element is added/removed from the list, and we | |
533 * need to omit animations for such temporary items. | |
534 * | |
535 * @param {ListItem=} opt_item The list item to be measured. | |
536 * @return {{height: number, marginTop: number, marginBottom:number, | |
537 * width: number, marginLeft: number, marginRight:number}} Size. | |
538 * @override | |
539 */ | |
540 NavigationList.prototype.measureItem = function(opt_item) { | |
541 this.measuringTemporaryItemNow_ = true; | |
542 var result = cr.ui.List.prototype.measureItem.call(this, opt_item); | |
543 this.measuringTemporaryItemNow_ = false; | |
544 return result; | |
545 }; | |
546 | |
547 /** | |
548 * Creates an element of a navigation list. This method is called from | |
549 * cr.ui.List internally. | |
550 * | |
551 * @param {NavigationModelItem} modelItem NavigationModelItem to be rendered. | |
552 * @return {NavigationListItem} Rendered element. | |
553 * @private | |
554 */ | |
555 NavigationList.prototype.renderRoot_ = function(modelItem) { | |
556 var item = new NavigationListItem(); | |
557 var volumeInfo = | |
558 PathUtil.isRootPath(modelItem.path) && | |
559 this.volumeManager_.getVolumeInfo(modelItem.path); | |
560 item.setModelItem(modelItem, volumeInfo && volumeInfo.deviceType); | |
561 | |
562 var handleClick = function() { | |
563 if (item.selected && | |
564 modelItem.path !== this.directoryModel_.getCurrentDirPath()) { | |
565 metrics.recordUserAction('FolderShortcut.Navigate'); | |
566 this.changeDirectory_(modelItem); | |
567 } | |
568 }.bind(this); | |
569 item.addEventListener('click', handleClick); | |
570 | |
571 var handleEject = function() { | |
572 var unmountCommand = cr.doc.querySelector('command#unmount'); | |
573 // Let's make sure 'canExecute' state of the command is properly set for | |
574 // the root before executing it. | |
575 unmountCommand.canExecuteChange(item); | |
576 unmountCommand.execute(item); | |
577 }; | |
578 item.addEventListener('eject', handleEject); | |
579 | |
580 if (this.contextMenu_) | |
581 item.maybeSetContextMenu(this.contextMenu_); | |
582 | |
583 return item; | |
584 }; | |
585 | |
586 /** | |
587 * Changes the current directory to the given path. | |
588 * If the given path is not found, a 'shortcut-target-not-found' event is | |
589 * fired. | |
590 * | |
591 * @param {NavigationModelItem} modelItem Directory to be chagned to. | |
592 * @private | |
593 */ | |
594 NavigationList.prototype.changeDirectory_ = function(modelItem) { | |
595 var onErrorCallback = function(error) { | |
596 if (error.code === FileError.NOT_FOUND_ERR) | |
597 this.dataModel.onItemNotFoundError(modelItem); | |
598 }.bind(this); | |
599 | |
600 this.directoryModel_.changeDirectory(modelItem.path, onErrorCallback); | |
601 }; | |
602 | |
603 /** | |
604 * Sets a context menu. Context menu is enabled only on archive and removable | |
605 * volumes as of now. | |
606 * | |
607 * @param {cr.ui.Menu} menu Context menu. | |
608 */ | |
609 NavigationList.prototype.setContextMenu = function(menu) { | |
610 this.contextMenu_ = menu; | |
611 | |
612 for (var i = 0; i < this.dataModel.length; i++) { | |
613 this.getListItemByIndex(i).maybeSetContextMenu(this.contextMenu_); | |
614 } | |
615 }; | |
616 | |
617 /** | |
618 * Selects the n-th item from the list. | |
619 * | |
620 * @param {number} index Item index. | |
621 * @return {boolean} True for success, otherwise false. | |
622 */ | |
623 NavigationList.prototype.selectByIndex = function(index) { | |
624 if (index < 0 || index > this.dataModel.length - 1) | |
625 return false; | |
626 | |
627 var newModelItem = this.dataModel.item(index); | |
628 var newPath = newModelItem.path; | |
629 if (!newPath) | |
630 return false; | |
631 | |
632 // Prevents double-moving to the current directory. | |
633 // eg. When user clicks the item, changing directory has already been done in | |
634 // click handler. | |
635 var entry = this.directoryModel_.getCurrentDirEntry(); | |
636 if (entry && entry.fullPath == newPath) | |
637 return false; | |
638 | |
639 metrics.recordUserAction('FolderShortcut.Navigate'); | |
640 this.changeDirectory_(newModelItem); | |
641 return true; | |
642 }; | |
643 | |
644 /** | |
645 * Handler before root item change. | |
646 * @param {Event} event The event. | |
647 * @private | |
648 */ | |
649 NavigationList.prototype.onBeforeSelectionChange_ = function(event) { | |
650 if (event.changes.length == 1 && !event.changes[0].selected) | |
651 event.preventDefault(); | |
652 }; | |
653 | |
654 /** | |
655 * Handler for root item being clicked. | |
656 * @param {Event} event The event. | |
657 * @private | |
658 */ | |
659 NavigationList.prototype.onSelectionChange_ = function(event) { | |
660 // This handler is invoked even when the navigation list itself changes the | |
661 // selection. In such case, we shouldn't handle the event. | |
662 if (this.dontHandleSelectionEvent_) | |
663 return; | |
664 | |
665 this.selectByIndex(this.selectionModel.selectedIndex); | |
666 }; | |
667 | |
668 /** | |
669 * Invoked when the current directory is changed. | |
670 * @param {Event} event The event. | |
671 * @private | |
672 */ | |
673 NavigationList.prototype.onCurrentDirectoryChanged_ = function(event) { | |
674 this.selectBestMatchItem_(); | |
675 }; | |
676 | |
677 /** | |
678 * Invoked when the content in the data model is changed. | |
679 * @param {Event} event The event. | |
680 * @private | |
681 */ | |
682 NavigationList.prototype.onListContentChanged_ = function(event) { | |
683 this.selectBestMatchItem_(); | |
684 }; | |
685 | |
686 /** | |
687 * Synchronizes the volume list selection with the current directory, after | |
688 * it is changed outside of the volume list. | |
689 * @private | |
690 */ | |
691 NavigationList.prototype.selectBestMatchItem_ = function() { | |
692 var entry = this.directoryModel_.getCurrentDirEntry(); | |
693 var path = entry && entry.fullPath; | |
694 if (!path) | |
695 return; | |
696 | |
697 // (1) Select the nearest parent directory (including the shortcut folders). | |
698 var bestMatchIndex = -1; | |
699 var bestMatchSubStringLen = 0; | |
700 for (var i = 0; i < this.dataModel.length; i++) { | |
701 var itemPath = this.dataModel.item(i).path; | |
702 if (path.indexOf(itemPath) == 0) { | |
703 if (bestMatchSubStringLen < itemPath.length) { | |
704 bestMatchIndex = i; | |
705 bestMatchSubStringLen = itemPath.length; | |
706 } | |
707 } | |
708 } | |
709 if (bestMatchIndex != -1) { | |
710 // Not to invoke the handler of this instance, sets the guard. | |
711 this.dontHandleSelectionEvent_ = true; | |
712 this.selectionModel.selectedIndex = bestMatchIndex; | |
713 this.dontHandleSelectionEvent_ = false; | |
714 return; | |
715 } | |
716 | |
717 // (2) Selects the volume of the current directory. | |
718 var newRootPath = PathUtil.getRootPath(path); | |
719 for (var i = 0; i < this.dataModel.length; i++) { | |
720 var itemPath = this.dataModel.item(i).path; | |
721 if (PathUtil.getRootPath(itemPath) == newRootPath) { | |
722 // Not to invoke the handler of this instance, sets the guard. | |
723 this.dontHandleSelectionEvent_ = true; | |
724 this.selectionModel.selectedIndex = i; | |
725 this.dontHandleSelectionEvent_ = false; | |
726 return; | |
727 } | |
728 } | |
729 }; | |
OLD | NEW |