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.
8 * A navigation list item.
10 * @extends {HTMLLIElement}
12 var NavigationListItem = cr.ui.define('li');
14 NavigationListItem.prototype = {
15 __proto__: HTMLLIElement.prototype,
16 get modelItem() { return this.modelItem_; }
22 NavigationListItem.prototype.decorate = function() {
23 // decorate() may be called twice: from the constructor and from
24 // List.createItem(). This check prevents double-decorating.
28 this.className = 'root-item';
29 this.setAttribute('role', 'option');
31 this.iconDiv_ = cr.doc.createElement('div');
32 this.iconDiv_.className = 'volume-icon';
33 this.appendChild(this.iconDiv_);
35 this.label_ = cr.doc.createElement('div');
36 this.label_.className = 'root-label';
37 this.appendChild(this.label_);
39 cr.defineProperty(this, 'lead', cr.PropertyKind.BOOL_ATTR);
40 cr.defineProperty(this, 'selected', cr.PropertyKind.BOOL_ATTR);
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.
49 NavigationListItem.prototype.setModelItem =
50 function(modelItem, opt_deviceType) {
52 console.warn('NavigationListItem.setModelItem should be called only once.');
54 this.modelItem_ = modelItem;
56 var rootType = PathUtil.getRootType(modelItem.path);
57 this.iconDiv_.setAttribute('volume-type-icon', rootType);
59 this.iconDiv_.setAttribute('volume-subtype', opt_deviceType);
62 this.label_.textContent = PathUtil.getFolderLabel(modelItem.path);
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() });
72 this.eject_.className = 'root-eject';
73 this.eject_.addEventListener('click', function(event) {
74 event.stopPropagation();
75 cr.dispatchSimpleEvent(this, 'eject');
78 this.appendChild(this.eject_);
83 * Associate a context menu with this item.
84 * @param {cr.ui.Menu} menu Menu this item.
86 NavigationListItem.prototype.maybeSetContextMenu = function(menu) {
87 if (!this.modelItem_.path) {
88 console.error('NavigationListItem.maybeSetContextMenu must be called ' +
89 'after setModelItem().');
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
99 (rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS))
100 cr.ui.contextMenuHandler.setContextMenu(this, menu);
106 * @extends {cr.ui.List}
108 function NavigationList() {
112 * NavigationList inherits cr.ui.List.
114 NavigationList.prototype = {
115 __proto__: cr.ui.List.prototype,
117 set dataModel(dataModel) {
118 if (!this.onListContentChangedBound_)
119 this.onListContentChangedBound_ = this.onListContentChanged_.bind(this);
121 if (this.dataModel_) {
122 this.dataModel_.removeEventListener(
123 'change', this.onListContentChangedBound_);
124 this.dataModel_.removeEventListener(
125 'permuted', this.onListContentChangedBound_);
128 var parentSetter = cr.ui.List.prototype.__lookupSetter__('dataModel');
129 parentSetter.call(this, dataModel);
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_);
138 return this.dataModel_;
141 // TODO(yoshiki): Add a setter of 'directoryModel'.
145 * @param {HTMLElement} el Element to be DirectoryItem.
146 * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system.
147 * @param {DirectoryModel} directoryModel Current DirectoryModel.
150 NavigationList.decorate = function(el, volumeManager, directoryModel) {
151 el.__proto__ = NavigationList.prototype;
152 el.decorate(volumeManager, directoryModel);
156 * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system.
157 * @param {DirectoryModel} directoryModel Current DirectoryModel.
159 NavigationList.prototype.decorate = function(volumeManager, directoryModel) {
160 cr.ui.List.decorate(this);
161 this.__proto__ = NavigationList.prototype;
163 this.directoryModel_ = directoryModel;
164 this.volumeManager_ = volumeManager;
165 this.selectionModel = new cr.ui.ListSingleSelectionModel();
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));
174 this.scrollBar_ = new ScrollBar();
175 this.scrollBar_.initialize(this.parentNode, this);
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');
182 this.itemConstructor = function(modelItem) {
183 return self.renderRoot_(modelItem);
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.
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.
197 NavigationList.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;
205 * Creates an element of a navigation list. This method is called from
206 * cr.ui.List internally.
208 * @param {NavigationModelItem} modelItem NavigationModelItem to be rendered.
209 * @return {NavigationListItem} Rendered element.
212 NavigationList.prototype.renderRoot_ = function(modelItem) {
213 var item = new NavigationListItem();
215 PathUtil.isRootPath(modelItem.path) &&
216 this.volumeManager_.getVolumeInfo(modelItem.path);
217 item.setModelItem(modelItem, volumeInfo && volumeInfo.deviceType);
219 var handleClick = function() {
221 modelItem.path !== this.directoryModel_.getCurrentDirPath()) {
222 metrics.recordUserAction('FolderShortcut.Navigate');
223 this.changeDirectory_(modelItem);
226 item.addEventListener('click', handleClick);
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);
235 item.addEventListener('eject', handleEject);
237 if (this.contextMenu_)
238 item.maybeSetContextMenu(this.contextMenu_);
244 * Changes the current directory to the given path.
245 * If the given path is not found, a 'shortcut-target-not-found' event is
248 * @param {NavigationModelItem} modelItem Directory to be chagned to.
251 NavigationList.prototype.changeDirectory_ = function(modelItem) {
252 var onErrorCallback = function(error) {
253 if (error.code === FileError.NOT_FOUND_ERR)
254 this.dataModel.onItemNotFoundError(modelItem);
257 this.directoryModel_.changeDirectory(modelItem.path, onErrorCallback);
261 * Sets a context menu. Context menu is enabled only on archive and removable
264 * @param {cr.ui.Menu} menu Context menu.
266 NavigationList.prototype.setContextMenu = function(menu) {
267 this.contextMenu_ = menu;
269 for (var i = 0; i < this.dataModel.length; i++) {
270 this.getListItemByIndex(i).maybeSetContextMenu(this.contextMenu_);
275 * Selects the n-th item from the list.
277 * @param {number} index Item index.
278 * @return {boolean} True for success, otherwise false.
280 NavigationList.prototype.selectByIndex = function(index) {
281 if (index < 0 || index > this.dataModel.length - 1)
284 var newModelItem = this.dataModel.item(index);
285 var newPath = newModelItem.path;
289 // Prevents double-moving to the current directory.
290 // eg. When user clicks the item, changing directory has already been done in
292 var entry = this.directoryModel_.getCurrentDirEntry();
293 if (entry && entry.fullPath == newPath)
296 metrics.recordUserAction('FolderShortcut.Navigate');
297 this.changeDirectory_(newModelItem);
302 * Handler before root item change.
303 * @param {Event} event The event.
306 NavigationList.prototype.onBeforeSelectionChange_ = function(event) {
307 if (event.changes.length == 1 && !event.changes[0].selected)
308 event.preventDefault();
312 * Handler for root item being clicked.
313 * @param {Event} event The event.
316 NavigationList.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_)
322 this.selectByIndex(this.selectionModel.selectedIndex);
326 * Invoked when the current directory is changed.
327 * @param {Event} event The event.
330 NavigationList.prototype.onCurrentDirectoryChanged_ = function(event) {
331 this.selectBestMatchItem_();
335 * Invoked when the content in the data model is changed.
336 * @param {Event} event The event.
339 NavigationList.prototype.onListContentChanged_ = function(event) {
340 this.selectBestMatchItem_();
344 * Synchronizes the volume list selection with the current directory, after
345 * it is changed outside of the volume list.
348 NavigationList.prototype.selectBestMatchItem_ = function() {
349 var entry = this.directoryModel_.getCurrentDirEntry();
350 var path = entry && entry.fullPath;
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) {
362 bestMatchSubStringLen = itemPath.length;
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;
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;