• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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