| 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 |