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 * A navigation list item. 9 * @constructor 10 * @extends {HTMLLIElement} 11 */ 12var NavigationListItem = cr.ui.define('li'); 13 14NavigationListItem.prototype = { 15 __proto__: HTMLLIElement.prototype, 16 get modelItem() { return this.modelItem_; } 17}; 18 19/** 20 * Decorate the item. 21 */ 22NavigationListItem.prototype.decorate = function() { 23 // decorate() may be called twice: from the constructor and from 24 // List.createItem(). This check prevents double-decorating. 25 if (this.className) 26 return; 27 28 this.className = 'root-item'; 29 this.setAttribute('role', 'option'); 30 31 this.iconDiv_ = cr.doc.createElement('div'); 32 this.iconDiv_.className = 'volume-icon'; 33 this.appendChild(this.iconDiv_); 34 35 this.label_ = cr.doc.createElement('div'); 36 this.label_.className = 'root-label'; 37 this.appendChild(this.label_); 38 39 cr.defineProperty(this, 'lead', cr.PropertyKind.BOOL_ATTR); 40 cr.defineProperty(this, 'selected', cr.PropertyKind.BOOL_ATTR); 41}; 42 43/** 44 * Associate a path with this item. 45 * @param {NavigationModelItem} modelItem NavigationModelItem of this item. 46 * @param {string=} opt_deviceType The type of the device. Available iff the 47 * path represents removable storage. 48 */ 49NavigationListItem.prototype.setModelItem = 50 function(modelItem, opt_deviceType) { 51 if (this.modelItem_) 52 console.warn('NavigationListItem.setModelItem should be called only once.'); 53 54 this.modelItem_ = modelItem; 55 56 var rootType = PathUtil.getRootType(modelItem.path); 57 this.iconDiv_.setAttribute('volume-type-icon', rootType); 58 if (opt_deviceType) { 59 this.iconDiv_.setAttribute('volume-subtype', opt_deviceType); 60 } 61 62 this.label_.textContent = modelItem.label; 63 64 if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) { 65 this.eject_ = cr.doc.createElement('div'); 66 // Block other mouse handlers. 67 this.eject_.addEventListener( 68 'mouseup', function(event) { event.stopPropagation() }); 69 this.eject_.addEventListener( 70 'mousedown', function(event) { event.stopPropagation() }); 71 72 this.eject_.className = 'root-eject'; 73 this.eject_.addEventListener('click', function(event) { 74 event.stopPropagation(); 75 cr.dispatchSimpleEvent(this, 'eject'); 76 }.bind(this)); 77 78 this.appendChild(this.eject_); 79 } 80}; 81 82/** 83 * Associate a context menu with this item. 84 * @param {cr.ui.Menu} menu Menu this item. 85 */ 86NavigationListItem.prototype.maybeSetContextMenu = function(menu) { 87 if (!this.modelItem_.path) { 88 console.error('NavigationListItem.maybeSetContextMenu must be called ' + 89 'after setModelItem().'); 90 return; 91 } 92 93 var isRoot = PathUtil.isRootPath(this.modelItem_.path); 94 var rootType = PathUtil.getRootType(this.modelItem_.path); 95 // The context menu is shown on the following items: 96 // - Removable and Archive volumes 97 // - Folder shortcuts 98 if (!isRoot || 99 (rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS)) 100 cr.ui.contextMenuHandler.setContextMenu(this, menu); 101}; 102 103/** 104 * A navigation list. 105 * @constructor 106 * @extends {cr.ui.List} 107 */ 108function NavigationList() { 109} 110 111/** 112 * NavigationList inherits cr.ui.List. 113 */ 114NavigationList.prototype = { 115 __proto__: cr.ui.List.prototype, 116 117 set dataModel(dataModel) { 118 if (!this.onListContentChangedBound_) 119 this.onListContentChangedBound_ = this.onListContentChanged_.bind(this); 120 121 if (this.dataModel_) { 122 this.dataModel_.removeEventListener( 123 'change', this.onListContentChangedBound_); 124 this.dataModel_.removeEventListener( 125 'permuted', this.onListContentChangedBound_); 126 } 127 128 var parentSetter = cr.ui.List.prototype.__lookupSetter__('dataModel'); 129 parentSetter.call(this, dataModel); 130 131 // This must be placed after the parent method is called, in order to make 132 // it sure that the list was changed. 133 dataModel.addEventListener('change', this.onListContentChangedBound_); 134 dataModel.addEventListener('permuted', this.onListContentChangedBound_); 135 }, 136 137 get dataModel() { 138 return this.dataModel_; 139 }, 140 141 // TODO(yoshiki): Add a setter of 'directoryModel'. 142}; 143 144/** 145 * @param {HTMLElement} el Element to be DirectoryItem. 146 * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system. 147 * @param {DirectoryModel} directoryModel Current DirectoryModel. 148 * folders. 149 */ 150NavigationList.decorate = function(el, volumeManager, directoryModel) { 151 el.__proto__ = NavigationList.prototype; 152 el.decorate(volumeManager, directoryModel); 153}; 154 155/** 156 * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system. 157 * @param {DirectoryModel} directoryModel Current DirectoryModel. 158 */ 159NavigationList.prototype.decorate = function(volumeManager, directoryModel) { 160 cr.ui.List.decorate(this); 161 this.__proto__ = NavigationList.prototype; 162 163 this.directoryModel_ = directoryModel; 164 this.volumeManager_ = volumeManager; 165 this.selectionModel = new cr.ui.ListSingleSelectionModel(); 166 167 this.directoryModel_.addEventListener('directory-changed', 168 this.onCurrentDirectoryChanged_.bind(this)); 169 this.selectionModel.addEventListener( 170 'change', this.onSelectionChange_.bind(this)); 171 this.selectionModel.addEventListener( 172 'beforeChange', this.onBeforeSelectionChange_.bind(this)); 173 174 this.scrollBar_ = new ScrollBar(); 175 this.scrollBar_.initialize(this.parentNode, this); 176 177 // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox' 178 // role for better accessibility on ChromeOS. 179 this.setAttribute('role', 'listbox'); 180 181 var self = this; 182 this.itemConstructor = function(modelItem) { 183 return self.renderRoot_(modelItem); 184 }; 185}; 186 187/** 188 * This overrides cr.ui.List.measureItem(). 189 * In the method, a temporary element is added/removed from the list, and we 190 * need to omit animations for such temporary items. 191 * 192 * @param {ListItem=} opt_item The list item to be measured. 193 * @return {{height: number, marginTop: number, marginBottom:number, 194 * width: number, marginLeft: number, marginRight:number}} Size. 195 * @override 196 */ 197NavigationList.prototype.measureItem = function(opt_item) { 198 this.measuringTemporaryItemNow_ = true; 199 var result = cr.ui.List.prototype.measureItem.call(this, opt_item); 200 this.measuringTemporaryItemNow_ = false; 201 return result; 202}; 203 204/** 205 * Creates an element of a navigation list. This method is called from 206 * cr.ui.List internally. 207 * 208 * @param {NavigationModelItem} modelItem NavigationModelItem to be rendered. 209 * @return {NavigationListItem} Rendered element. 210 * @private 211 */ 212NavigationList.prototype.renderRoot_ = function(modelItem) { 213 var item = new NavigationListItem(); 214 var volumeInfo = 215 PathUtil.isRootPath(modelItem.path) && 216 this.volumeManager_.getVolumeInfo(modelItem.path); 217 item.setModelItem(modelItem, volumeInfo && volumeInfo.deviceType); 218 219 var handleClick = function() { 220 if (item.selected && 221 modelItem.path !== this.directoryModel_.getCurrentDirPath()) { 222 metrics.recordUserAction('FolderShortcut.Navigate'); 223 this.changeDirectory_(modelItem); 224 } 225 }.bind(this); 226 item.addEventListener('click', handleClick); 227 228 var handleEject = function() { 229 var unmountCommand = cr.doc.querySelector('command#unmount'); 230 // Let's make sure 'canExecute' state of the command is properly set for 231 // the root before executing it. 232 unmountCommand.canExecuteChange(item); 233 unmountCommand.execute(item); 234 }; 235 item.addEventListener('eject', handleEject); 236 237 if (this.contextMenu_) 238 item.maybeSetContextMenu(this.contextMenu_); 239 240 return item; 241}; 242 243/** 244 * Changes the current directory to the given path. 245 * If the given path is not found, a 'shortcut-target-not-found' event is 246 * fired. 247 * 248 * @param {NavigationModelItem} modelItem Directory to be chagned to. 249 * @private 250 */ 251NavigationList.prototype.changeDirectory_ = function(modelItem) { 252 var onErrorCallback = function(error) { 253 if (error.code === FileError.NOT_FOUND_ERR) 254 this.dataModel.onItemNotFoundError(modelItem); 255 }.bind(this); 256 257 this.directoryModel_.changeDirectory(modelItem.path, onErrorCallback); 258}; 259 260/** 261 * Sets a context menu. Context menu is enabled only on archive and removable 262 * volumes as of now. 263 * 264 * @param {cr.ui.Menu} menu Context menu. 265 */ 266NavigationList.prototype.setContextMenu = function(menu) { 267 this.contextMenu_ = menu; 268 269 for (var i = 0; i < this.dataModel.length; i++) { 270 this.getListItemByIndex(i).maybeSetContextMenu(this.contextMenu_); 271 } 272}; 273 274/** 275 * Selects the n-th item from the list. 276 * 277 * @param {number} index Item index. 278 * @return {boolean} True for success, otherwise false. 279 */ 280NavigationList.prototype.selectByIndex = function(index) { 281 if (index < 0 || index > this.dataModel.length - 1) 282 return false; 283 284 var newModelItem = this.dataModel.item(index); 285 var newPath = newModelItem.path; 286 if (!newPath) 287 return false; 288 289 // Prevents double-moving to the current directory. 290 // eg. When user clicks the item, changing directory has already been done in 291 // click handler. 292 var entry = this.directoryModel_.getCurrentDirEntry(); 293 if (entry && entry.fullPath == newPath) 294 return false; 295 296 metrics.recordUserAction('FolderShortcut.Navigate'); 297 this.changeDirectory_(newModelItem); 298 return true; 299}; 300 301/** 302 * Handler before root item change. 303 * @param {Event} event The event. 304 * @private 305 */ 306NavigationList.prototype.onBeforeSelectionChange_ = function(event) { 307 if (event.changes.length == 1 && !event.changes[0].selected) 308 event.preventDefault(); 309}; 310 311/** 312 * Handler for root item being clicked. 313 * @param {Event} event The event. 314 * @private 315 */ 316NavigationList.prototype.onSelectionChange_ = function(event) { 317 // This handler is invoked even when the navigation list itself changes the 318 // selection. In such case, we shouldn't handle the event. 319 if (this.dontHandleSelectionEvent_) 320 return; 321 322 this.selectByIndex(this.selectionModel.selectedIndex); 323}; 324 325/** 326 * Invoked when the current directory is changed. 327 * @param {Event} event The event. 328 * @private 329 */ 330NavigationList.prototype.onCurrentDirectoryChanged_ = function(event) { 331 this.selectBestMatchItem_(); 332}; 333 334/** 335 * Invoked when the content in the data model is changed. 336 * @param {Event} event The event. 337 * @private 338 */ 339NavigationList.prototype.onListContentChanged_ = function(event) { 340 this.selectBestMatchItem_(); 341}; 342 343/** 344 * Synchronizes the volume list selection with the current directory, after 345 * it is changed outside of the volume list. 346 * @private 347 */ 348NavigationList.prototype.selectBestMatchItem_ = function() { 349 var entry = this.directoryModel_.getCurrentDirEntry(); 350 var path = entry && entry.fullPath; 351 if (!path) 352 return; 353 354 // (1) Select the nearest parent directory (including the shortcut folders). 355 var bestMatchIndex = -1; 356 var bestMatchSubStringLen = 0; 357 for (var i = 0; i < this.dataModel.length; i++) { 358 var itemPath = this.dataModel.item(i).path; 359 if (path.indexOf(itemPath) == 0) { 360 if (bestMatchSubStringLen < itemPath.length) { 361 bestMatchIndex = i; 362 bestMatchSubStringLen = itemPath.length; 363 } 364 } 365 } 366 if (bestMatchIndex != -1) { 367 // Not to invoke the handler of this instance, sets the guard. 368 this.dontHandleSelectionEvent_ = true; 369 this.selectionModel.selectedIndex = bestMatchIndex; 370 this.dontHandleSelectionEvent_ = false; 371 return; 372 } 373 374 // (2) Selects the volume of the current directory. 375 var newRootPath = PathUtil.getRootPath(path); 376 for (var i = 0; i < this.dataModel.length; i++) { 377 var itemPath = this.dataModel.item(i).path; 378 if (PathUtil.getRootPath(itemPath) == newRootPath) { 379 // Not to invoke the handler of this instance, sets the guard. 380 this.dontHandleSelectionEvent_ = true; 381 this.selectionModel.selectedIndex = i; 382 this.dontHandleSelectionEvent_ = false; 383 return; 384 } 385 } 386}; 387