| 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 * A volume list model. This model combines the 2 lists. | |
| 9 * @param {cr.ui.ArrayDataModel} volumesList The first list of the model. | |
| 10 * @param {cr.ui.ArrayDataModel} pinnedList The second list of the model. | |
| 11 * @constructor | |
| 12 * @extends {cr.EventTarget} | |
| 13 */ | |
| 14 function VolumeListModel(volumesList, pinnedList) { | |
| 15 this.volumesList_ = volumesList; | |
| 16 this.pinnedList_ = pinnedList; | |
| 17 | |
| 18 // Generates a combined 'permuted' event from an event of either list. | |
| 19 var permutedHandler = function(listNum, e) { | |
| 20 var permutedEvent = new Event('permuted'); | |
| 21 var newPermutation = []; | |
| 22 var newLength; | |
| 23 if (listNum == 1) { | |
| 24 newLength = e.newLength + this.pinnedList_.length; | |
| 25 for (var i = 0; i < e.permutation.length; i++) { | |
| 26 newPermutation[i] = e.permutation[i]; | |
| 27 } | |
| 28 for (var i = 0; i < this.pinnedList_.length; i++) { | |
| 29 newPermutation[i + e.permutation.length] = i + e.newLength; | |
| 30 } | |
| 31 } else { | |
| 32 var volumesLen = this.volumesList_.length; | |
| 33 newLength = e.newLength + volumesLen; | |
| 34 for (var i = 0; i < volumesLen; i++) { | |
| 35 newPermutation[i] = i; | |
| 36 } | |
| 37 for (var i = 0; i < e.permutation.length; i++) { | |
| 38 newPermutation[i + volumesLen] = | |
| 39 (e.permutation[i] !== -1) ? (e.permutation[i] + volumesLen) : -1; | |
| 40 } | |
| 41 } | |
| 42 | |
| 43 permutedEvent.newLength = newLength; | |
| 44 permutedEvent.permutation = newPermutation; | |
| 45 this.dispatchEvent(permutedEvent); | |
| 46 }; | |
| 47 this.volumesList_.addEventListener('permuted', permutedHandler.bind(this, 1)); | |
| 48 this.pinnedList_.addEventListener('permuted', permutedHandler.bind(this, 2)); | |
| 49 | |
| 50 // Generates a combined 'change' event from an event of either list. | |
| 51 var changeHandler = function(listNum, e) { | |
| 52 var changeEvent = new Event('change'); | |
| 53 changeEvent.index = | |
| 54 (listNum == 1) ? e.index : (e.index + this.volumesList_.length); | |
| 55 this.dispatchEvent(changeEvent); | |
| 56 }; | |
| 57 this.volumesList_.addEventListener('change', changeHandler.bind(this, 1)); | |
| 58 this.pinnedList_.addEventListener('change', changeHandler.bind(this, 2)); | |
| 59 | |
| 60 // 'splice' and 'sorted' events are not implemented, since they are not used | |
| 61 // in list.js. | |
| 62 } | |
| 63 | |
| 64 /** | |
| 65 * VolumeList inherits cr.EventTarget. | |
| 66 */ | |
| 67 VolumeListModel.prototype = { | |
| 68 __proto__: cr.EventTarget.prototype, | |
| 69 get length() { return this.length_(); } | |
| 70 }; | |
| 71 | |
| 72 /** | |
| 73 * Returns the item at the given index. | |
| 74 * @param {number} index The index of the entry to get. | |
| 75 * @return {?string} The path at the given index. | |
| 76 */ | |
| 77 VolumeListModel.prototype.item = function(index) { | |
| 78 var offset = this.volumesList_.length; | |
| 79 if (index < offset) { | |
| 80 var entry = this.volumesList_.item(index); | |
| 81 return entry ? entry.fullPath : undefined; | |
| 82 } else { | |
| 83 return this.pinnedList_.item(index - offset); | |
| 84 } | |
| 85 }; | |
| 86 | |
| 87 /** | |
| 88 * Type of the item on the volume list. | |
| 89 * @enum {number} | |
| 90 */ | |
| 91 VolumeListModel.ItemType = { | |
| 92 ROOT: 1, | |
| 93 PINNED: 2 | |
| 94 }; | |
| 95 | |
| 96 /** | |
| 97 * Returns the type of the item at the given index. | |
| 98 * @param {number} index The index of the entry to get. | |
| 99 * @return {VolumeListModel.ItemType} The type of the item. | |
| 100 */ | |
| 101 VolumeListModel.prototype.getItemType = function(index) { | |
| 102 var offset = this.volumesList_.length; | |
| 103 return index < offset ? | |
| 104 VolumeListModel.ItemType.ROOT : VolumeListModel.ItemType.PINNED; | |
| 105 }; | |
| 106 | |
| 107 /** | |
| 108 * Returns the number of items in the model. | |
| 109 * @return {number} The length of the model. | |
| 110 * @private | |
| 111 */ | |
| 112 VolumeListModel.prototype.length_ = function() { | |
| 113 return this.volumesList_.length + this.pinnedList_.length; | |
| 114 }; | |
| 115 | |
| 116 /** | |
| 117 * Returns the first matching item. | |
| 118 * @param {Entry} item The entry to find. | |
| 119 * @param {number=} opt_fromIndex If provided, then the searching start at | |
| 120 * the {@code opt_fromIndex}. | |
| 121 * @return {number} The index of the first found element or -1 if not found. | |
| 122 */ | |
| 123 VolumeListModel.prototype.indexOf = function(item, opt_fromIndex) { | |
| 124 for (var i = opt_fromIndex || 0; i < this.length; i++) { | |
| 125 if (item === this.item(i)) | |
| 126 return i; | |
| 127 } | |
| 128 return -1; | |
| 129 }; | |
| 130 | |
| 131 /** | |
| 132 * A volume item. | |
| 133 * @constructor | |
| 134 * @extends {HTMLLIElement} | |
| 135 */ | |
| 136 var VolumeItem = cr.ui.define('li'); | |
| 137 | |
| 138 VolumeItem.prototype = { | |
| 139 __proto__: HTMLLIElement.prototype, | |
| 140 }; | |
| 141 | |
| 142 /** | |
| 143 * Decorate the item. | |
| 144 */ | |
| 145 VolumeItem.prototype.decorate = function() { | |
| 146 // decorate() may be called twice: from the constructor and from | |
| 147 // List.createItem(). This check prevents double-decorating. | |
| 148 if (this.className) | |
| 149 return; | |
| 150 | |
| 151 this.className = 'root-item'; | |
| 152 this.setAttribute('role', 'option'); | |
| 153 | |
| 154 this.iconDiv_ = cr.doc.createElement('div'); | |
| 155 this.iconDiv_.className = 'volume-icon'; | |
| 156 this.appendChild(this.iconDiv_); | |
| 157 | |
| 158 this.label_ = cr.doc.createElement('div'); | |
| 159 this.label_.className = 'root-label'; | |
| 160 this.appendChild(this.label_); | |
| 161 | |
| 162 cr.defineProperty(this, 'lead', cr.PropertyKind.BOOL_ATTR); | |
| 163 cr.defineProperty(this, 'selected', cr.PropertyKind.BOOL_ATTR); | |
| 164 }; | |
| 165 | |
| 166 /** | |
| 167 * Associate a path with this item. | |
| 168 * @param {string} path Path of this item. | |
| 169 */ | |
| 170 VolumeItem.prototype.setPath = function(path) { | |
| 171 if (this.path_) | |
| 172 console.warn('VolumeItem.setPath should be called only once.'); | |
| 173 | |
| 174 this.path_ = path; | |
| 175 | |
| 176 var rootType = PathUtil.getRootType(path); | |
| 177 | |
| 178 this.iconDiv_.setAttribute('volume-type-icon', rootType); | |
| 179 if (rootType === RootType.REMOVABLE) { | |
| 180 this.iconDiv_.setAttribute('volume-subtype', | |
| 181 VolumeManager.getInstance().getDeviceType(path)); | |
| 182 } | |
| 183 | |
| 184 this.label_.textContent = PathUtil.getFolderLabel(path); | |
| 185 | |
| 186 if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) { | |
| 187 this.eject_ = cr.doc.createElement('div'); | |
| 188 // Block other mouse handlers. | |
| 189 this.eject_.addEventListener( | |
| 190 'mouseup', function(e) { e.stopPropagation() }); | |
| 191 this.eject_.addEventListener( | |
| 192 'mousedown', function(e) { e.stopPropagation() }); | |
| 193 | |
| 194 this.eject_.className = 'root-eject'; | |
| 195 this.eject_.addEventListener('click', function(event) { | |
| 196 event.stopPropagation(); | |
| 197 cr.dispatchSimpleEvent(this, 'eject'); | |
| 198 }.bind(this)); | |
| 199 | |
| 200 this.appendChild(this.eject_); | |
| 201 } | |
| 202 }; | |
| 203 | |
| 204 /** | |
| 205 * Associate a context menu with this item. | |
| 206 * @param {cr.ui.Menu} menu Menu this item. | |
| 207 */ | |
| 208 VolumeItem.prototype.maybeSetContextMenu = function(menu) { | |
| 209 if (!this.path_) { | |
| 210 console.error( | |
| 211 'VolumeItem.maybeSetContextMenu must be called after setPath().'); | |
| 212 return; | |
| 213 } | |
| 214 | |
| 215 var isRoot = PathUtil.isRootPath(this.path_); | |
| 216 var rootType = PathUtil.getRootType(this.path_); | |
| 217 // The context menu is shown on the following items: | |
| 218 // - Removable and Archive volumes | |
| 219 // - Folder shortcuts | |
| 220 if (!isRoot || | |
| 221 (rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS)) | |
| 222 cr.ui.contextMenuHandler.setContextMenu(this, menu); | |
| 223 }; | |
| 224 | |
| 225 /** | |
| 226 * A volume list. | |
| 227 * @constructor | |
| 228 * @extends {cr.ui.List} | |
| 229 */ | |
| 230 function VolumeList() { | |
| 231 } | |
| 232 | |
| 233 /** | |
| 234 * VolumeList inherits cr.ui.List. | |
| 235 */ | |
| 236 VolumeList.prototype.__proto__ = cr.ui.List.prototype; | |
| 237 | |
| 238 /** | |
| 239 * @param {HTMLElement} el Element to be DirectoryItem. | |
| 240 * @param {DirectoryModel} directoryModel Current DirectoryModel. | |
| 241 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned | |
| 242 * folders. | |
| 243 */ | |
| 244 VolumeList.decorate = function(el, directoryModel, pinnedFolderModel) { | |
| 245 el.__proto__ = VolumeList.prototype; | |
| 246 el.decorate(directoryModel, pinnedFolderModel); | |
| 247 }; | |
| 248 | |
| 249 /** | |
| 250 * @param {DirectoryModel} directoryModel Current DirectoryModel. | |
| 251 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned | |
| 252 * folders. | |
| 253 */ | |
| 254 VolumeList.prototype.decorate = function(directoryModel, pinnedFolderModel) { | |
| 255 cr.ui.List.decorate(this); | |
| 256 this.__proto__ = VolumeList.prototype; | |
| 257 | |
| 258 this.directoryModel_ = directoryModel; | |
| 259 this.volumeManager_ = VolumeManager.getInstance(); | |
| 260 this.selectionModel = new cr.ui.ListSingleSelectionModel(); | |
| 261 | |
| 262 this.directoryModel_.addEventListener('directory-changed', | |
| 263 this.onCurrentDirectoryChanged_.bind(this)); | |
| 264 this.selectionModel.addEventListener( | |
| 265 'change', this.onSelectionChange_.bind(this)); | |
| 266 this.selectionModel.addEventListener( | |
| 267 'beforeChange', this.onBeforeSelectionChange_.bind(this)); | |
| 268 | |
| 269 this.scrollBar_ = new ScrollBar(); | |
| 270 this.scrollBar_.initialize(this.parentNode, this); | |
| 271 | |
| 272 // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox' | |
| 273 // role for better accessibility on ChromeOS. | |
| 274 this.setAttribute('role', 'listbox'); | |
| 275 | |
| 276 var self = this; | |
| 277 this.itemConstructor = function(path) { | |
| 278 return self.renderRoot_(path); | |
| 279 }; | |
| 280 | |
| 281 this.pinnedItemList_ = pinnedFolderModel; | |
| 282 | |
| 283 this.dataModel = | |
| 284 new VolumeListModel(this.directoryModel_.getRootsList(), | |
| 285 this.pinnedItemList_); | |
| 286 }; | |
| 287 | |
| 288 /** | |
| 289 * Creates an element of a volume. This method is called from cr.ui.List | |
| 290 * internally. | |
| 291 * @param {string} path Path of the directory to be rendered. | |
| 292 * @return {VolumeItem} Rendered element. | |
| 293 * @private | |
| 294 */ | |
| 295 VolumeList.prototype.renderRoot_ = function(path) { | |
| 296 var item = new VolumeItem(); | |
| 297 item.setPath(path); | |
| 298 | |
| 299 var handleClick = function() { | |
| 300 if (item.selected && path !== this.directoryModel_.getCurrentDirPath()) { | |
| 301 this.directoryModel_.changeDirectory(path); | |
| 302 } | |
| 303 }.bind(this); | |
| 304 item.addEventListener('click', handleClick); | |
| 305 | |
| 306 var handleEject = function() { | |
| 307 var unmountCommand = cr.doc.querySelector('command#unmount'); | |
| 308 // Let's make sure 'canExecute' state of the command is properly set for | |
| 309 // the root before executing it. | |
| 310 unmountCommand.canExecuteChange(item); | |
| 311 unmountCommand.execute(item); | |
| 312 }; | |
| 313 item.addEventListener('eject', handleEject); | |
| 314 // TODO(yoshiki): Check if the following touch handler is necessary or not. | |
| 315 // If unnecessary, remove it. | |
| 316 item.addEventListener(cr.ui.TouchHandler.EventType.TOUCH_START, handleClick); | |
| 317 | |
| 318 if (this.contextMenu_) | |
| 319 item.maybeSetContextMenu(this.contextMenu_); | |
| 320 | |
| 321 // If the current directory is already set. | |
| 322 if (this.currentVolume_ == path) { | |
| 323 setTimeout(function() { | |
| 324 this.selectedItem = path; | |
| 325 }.bind(this), 0); | |
| 326 } | |
| 327 return item; | |
| 328 }; | |
| 329 | |
| 330 /** | |
| 331 * Sets a context menu. Context menu is enabled only on archive and removable | |
| 332 * volumes as of now. | |
| 333 * | |
| 334 * @param {cr.ui.Menu} menu Context menu. | |
| 335 */ | |
| 336 VolumeList.prototype.setContextMenu = function(menu) { | |
| 337 this.contextMenu_ = menu; | |
| 338 | |
| 339 for (var i = 0; i < this.dataModel.length; i++) { | |
| 340 this.getListItemByIndex(i).maybeSetContextMenu(this.contextMenu_); | |
| 341 } | |
| 342 }; | |
| 343 | |
| 344 /** | |
| 345 * Selects the n-th volume from the list. | |
| 346 * @param {number} index Volume index. | |
| 347 * @return {boolean} True for success, otherwise false. | |
| 348 */ | |
| 349 VolumeList.prototype.selectByIndex = function(index) { | |
| 350 if (index < 0 || index > this.dataModel.length - 1) | |
| 351 return false; | |
| 352 | |
| 353 var newPath = this.dataModel.item(index); | |
| 354 if (!newPath) | |
| 355 return false; | |
| 356 | |
| 357 // Prevents double-moving to the current directory. | |
| 358 if (this.directoryModel_.getCurrentDirEntry().fullPath == newPath) | |
| 359 return false; | |
| 360 | |
| 361 this.directoryModel_.changeDirectory(newPath); | |
| 362 return true; | |
| 363 }; | |
| 364 | |
| 365 /** | |
| 366 * Handler before root item change. | |
| 367 * @param {Event} event The event. | |
| 368 * @private | |
| 369 */ | |
| 370 VolumeList.prototype.onBeforeSelectionChange_ = function(event) { | |
| 371 if (event.changes.length == 1 && !event.changes[0].selected) | |
| 372 event.preventDefault(); | |
| 373 }; | |
| 374 | |
| 375 /** | |
| 376 * Handler for root item being clicked. | |
| 377 * @param {Event} event The event. | |
| 378 * @private | |
| 379 */ | |
| 380 VolumeList.prototype.onSelectionChange_ = function(event) { | |
| 381 // This handler is invoked even when the volume list itself changes the | |
| 382 // selection. In such case, we shouldn't handle the event. | |
| 383 if (this.dontHandleSelectionEvent_) | |
| 384 return; | |
| 385 | |
| 386 this.selectByIndex(this.selectionModel.selectedIndex); | |
| 387 }; | |
| 388 | |
| 389 /** | |
| 390 * Invoked when the current directory is changed. | |
| 391 * @param {Event} event The event. | |
| 392 * @private | |
| 393 */ | |
| 394 VolumeList.prototype.onCurrentDirectoryChanged_ = function(event) { | |
| 395 var path = event.newDirEntry.fullPath || this.dataModel.getCurrentDirPath(); | |
| 396 var newRootPath = PathUtil.getRootPath(path); | |
| 397 | |
| 398 // Synchronizes the volume list selection with the current directory, after | |
| 399 // it is changed outside of the volume list. | |
| 400 | |
| 401 // (1) Select the nearest parent directory (including the pinned directories). | |
| 402 var bestMatchIndex = -1; | |
| 403 var bestMatchSubStringLen = 0; | |
| 404 for (var i = 0; i < this.dataModel.length; i++) { | |
| 405 var itemPath = this.dataModel.item(i); | |
| 406 if (path.indexOf(itemPath) == 0) { | |
| 407 if (bestMatchSubStringLen < itemPath.length) { | |
| 408 bestMatchIndex = i; | |
| 409 bestMatchSubStringLen = itemPath.length; | |
| 410 } | |
| 411 } | |
| 412 } | |
| 413 if (bestMatchIndex != -1) { | |
| 414 // Not to invoke the handler of this instance, sets the guard. | |
| 415 this.dontHandleSelectionEvent_ = true; | |
| 416 this.selectionModel.selectedIndex = bestMatchIndex; | |
| 417 this.dontHandleSelectionEvent_ = false; | |
| 418 return; | |
| 419 } | |
| 420 | |
| 421 // (2) Selects the volume of the current directory. | |
| 422 for (var i = 0; i < this.dataModel.length; i++) { | |
| 423 var itemPath = this.dataModel.item(i); | |
| 424 if (PathUtil.getRootPath(itemPath) == newRootPath) { | |
| 425 // Not to invoke the handler of this instance, sets the guard. | |
| 426 this.dontHandleSelectionEvent_ = true; | |
| 427 this.selectionModel.selectedIndex = i; | |
| 428 this.dontHandleSelectionEvent_ = false; | |
| 429 return; | |
| 430 } | |
| 431 } | |
| 432 }; | |
| OLD | NEW |