• 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 entry-name';
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 model with this item.
45 * @param {NavigationModelItem} modelItem NavigationModelItem of this item.
46 */
47NavigationListItem.prototype.setModelItem = function(modelItem) {
48  if (this.modelItem_)
49    console.warn('NavigationListItem.setModelItem should be called only once.');
50
51  this.modelItem_ = modelItem;
52
53  var typeIcon;
54  if (modelItem.isVolume) {
55    if (modelItem.volumeInfo.volumeType == 'provided') {
56      var backgroundImage = '-webkit-image-set(' +
57          'url(chrome://extension-icon/' + modelItem.volumeInfo.extensionId +
58              '/24/1) 1x, ' +
59          'url(chrome://extension-icon/' + modelItem.volumeInfo.extensionId +
60              '/48/1) 2x);';
61      // The icon div is not yet added to DOM, therefore it is impossible to
62      // use style.backgroundImage.
63      this.iconDiv_.setAttribute(
64          'style', 'background-image: ' + backgroundImage);
65    }
66    typeIcon = modelItem.volumeInfo.volumeType;
67  } else if (modelItem.isShortcut) {
68    // Shortcuts are available for Drive only.
69    typeIcon = VolumeManagerCommon.VolumeType.DRIVE;
70  }
71
72  this.iconDiv_.setAttribute('volume-type-icon', typeIcon);
73
74  if (modelItem.isVolume) {
75    this.iconDiv_.setAttribute(
76        'volume-subtype', modelItem.volumeInfo.deviceType);
77  }
78
79  this.label_.textContent = modelItem.label;
80
81  if (modelItem.isVolume &&
82      (modelItem.volumeInfo.volumeType ===
83           VolumeManagerCommon.VolumeType.ARCHIVE ||
84       modelItem.volumeInfo.volumeType ===
85           VolumeManagerCommon.VolumeType.REMOVABLE ||
86       modelItem.volumeInfo.volumeType ===
87           VolumeManagerCommon.VolumeType.PROVIDED)) {
88    this.eject_ = cr.doc.createElement('div');
89    // Block other mouse handlers.
90    this.eject_.addEventListener(
91        'mouseup', function(event) { event.stopPropagation() });
92    this.eject_.addEventListener(
93        'mousedown', function(event) { event.stopPropagation() });
94
95    this.eject_.className = 'root-eject';
96    this.eject_.addEventListener('click', function(event) {
97      event.stopPropagation();
98      cr.dispatchSimpleEvent(this, 'eject');
99    }.bind(this));
100
101    this.appendChild(this.eject_);
102  }
103};
104
105/**
106 * Associate a context menu with this item.
107 * @param {cr.ui.Menu} menu Menu this item.
108 */
109NavigationListItem.prototype.maybeSetContextMenu = function(menu) {
110  if (!this.modelItem_) {
111    console.error('NavigationListItem.maybeSetContextMenu must be called ' +
112                  'after setModelItem().');
113    return;
114  }
115
116  // The context menu is shown on the following items:
117  // - Removable and Archive volumes
118  // - Folder shortcuts
119  if (this.modelItem_.isVolume && (this.modelItem_.volumeInfo.volumeType ===
120          VolumeManagerCommon.VolumeType.REMOVABLE ||
121      this.modelItem_.volumeInfo.volumeType ===
122          VolumeManagerCommon.VolumeType.ARCHIVE ||
123      this.modelItem_.volumeInfo.volumeType ===
124          VolumeManagerCommon.VolumeType.PROVIDED) ||
125      this.modelItem_.isShortcut) {
126    cr.ui.contextMenuHandler.setContextMenu(this, menu);
127  }
128};
129
130/**
131 * A navigation list.
132 * @constructor
133 * @extends {cr.ui.List}
134 */
135function NavigationList() {
136}
137
138/**
139 * NavigationList inherits cr.ui.List.
140 */
141NavigationList.prototype = {
142  __proto__: cr.ui.List.prototype,
143
144  set dataModel(dataModel) {
145    if (!this.onListContentChangedBound_)
146      this.onListContentChangedBound_ = this.onListContentChanged_.bind(this);
147
148    if (this.dataModel_) {
149      this.dataModel_.removeEventListener(
150          'change', this.onListContentChangedBound_);
151      this.dataModel_.removeEventListener(
152          'permuted', this.onListContentChangedBound_);
153    }
154
155    var parentSetter = cr.ui.List.prototype.__lookupSetter__('dataModel');
156    parentSetter.call(this, dataModel);
157
158    // This must be placed after the parent method is called, in order to make
159    // it sure that the list was changed.
160    dataModel.addEventListener('change', this.onListContentChangedBound_);
161    dataModel.addEventListener('permuted', this.onListContentChangedBound_);
162  },
163
164  get dataModel() {
165    return this.dataModel_;
166  },
167
168  // TODO(yoshiki): Add a setter of 'directoryModel'.
169};
170
171/**
172 * @param {HTMLElement} el Element to be DirectoryItem.
173 * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system.
174 * @param {DirectoryModel} directoryModel Current DirectoryModel.
175 *     folders.
176 */
177NavigationList.decorate = function(el, volumeManager, directoryModel) {
178  el.__proto__ = NavigationList.prototype;
179  el.decorate(volumeManager, directoryModel);
180};
181
182/**
183 * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system.
184 * @param {DirectoryModel} directoryModel Current DirectoryModel.
185 */
186NavigationList.prototype.decorate = function(volumeManager, directoryModel) {
187  cr.ui.List.decorate(this);
188  this.__proto__ = NavigationList.prototype;
189
190  this.directoryModel_ = directoryModel;
191  this.volumeManager_ = volumeManager;
192  this.selectionModel = new cr.ui.ListSingleSelectionModel();
193
194  this.directoryModel_.addEventListener('directory-changed',
195      this.onCurrentDirectoryChanged_.bind(this));
196  this.selectionModel.addEventListener(
197      'change', this.onSelectionChange_.bind(this));
198  this.selectionModel.addEventListener(
199      'beforeChange', this.onBeforeSelectionChange_.bind(this));
200
201  this.scrollBar_ = new ScrollBar();
202  this.scrollBar_.initialize(this.parentNode, this);
203
204  // Keeps track of selected model item to detect if it is changed actually.
205  this.currentModelItem_ = null;
206
207  // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox'
208  // role for better accessibility on ChromeOS.
209  this.setAttribute('role', 'listbox');
210
211  var self = this;
212  this.itemConstructor = function(modelItem) {
213    return self.renderRoot_(modelItem);
214  };
215};
216
217/**
218 * This overrides cr.ui.List.measureItem().
219 * In the method, a temporary element is added/removed from the list, and we
220 * need to omit animations for such temporary items.
221 *
222 * @param {ListItem=} opt_item The list item to be measured.
223 * @return {{height: number, marginTop: number, marginBottom:number,
224 *     width: number, marginLeft: number, marginRight:number}} Size.
225 * @override
226 */
227NavigationList.prototype.measureItem = function(opt_item) {
228  this.measuringTemporaryItemNow_ = true;
229  var result = cr.ui.List.prototype.measureItem.call(this, opt_item);
230  this.measuringTemporaryItemNow_ = false;
231  return result;
232};
233
234/**
235 * Creates an element of a navigation list. This method is called from
236 * cr.ui.List internally.
237 *
238 * @param {NavigationModelItem} modelItem NavigationModelItem to be rendered.
239 * @return {NavigationListItem} Rendered element.
240 * @private
241 */
242NavigationList.prototype.renderRoot_ = function(modelItem) {
243  var item = new NavigationListItem();
244  item.setModelItem(modelItem);
245
246  var handleClick = function() {
247    if (!item.selected)
248      return;
249    this.activateModelItem_(item.modelItem);
250    // On clicking the root item, clears the selection.
251    this.directoryModel_.clearSelection();
252  }.bind(this);
253  item.addEventListener('click', handleClick);
254
255  var handleEject = function() {
256    var unmountCommand = cr.doc.querySelector('command#unmount');
257    // Let's make sure 'canExecute' state of the command is properly set for
258    // the root before executing it.
259    unmountCommand.canExecuteChange(item);
260    unmountCommand.execute(item);
261  };
262  item.addEventListener('eject', handleEject);
263
264  if (this.contextMenu_)
265    item.maybeSetContextMenu(this.contextMenu_);
266
267  return item;
268};
269
270/**
271 * Sets a context menu. Context menu is enabled only on archive and removable
272 * volumes as of now.
273 *
274 * @param {cr.ui.Menu} menu Context menu.
275 */
276NavigationList.prototype.setContextMenu = function(menu) {
277  this.contextMenu_ = menu;
278
279  for (var i = 0; i < this.dataModel.length; i++) {
280    this.getListItemByIndex(i).maybeSetContextMenu(this.contextMenu_);
281  }
282};
283
284/**
285 * Selects the n-th item from the list.
286 * @param {number} index Item index.
287 * @return {boolean} True for success, otherwise false.
288 */
289NavigationList.prototype.selectByIndex = function(index) {
290  if (index < 0 || index > this.dataModel.length - 1)
291    return false;
292
293  this.selectionModel.selectedIndex = index;
294  this.activateModelItem_(this.dataModel.item(index));
295  return true;
296};
297
298
299/**
300 * Selects the passed item of the model.
301 * @param {NavigationModelItem} modelItem Model item to be activated.
302 * @private
303 */
304NavigationList.prototype.activateModelItem_ = function(modelItem) {
305  var onEntryResolved = function(entry) {
306    // Changes directory to the model item's root directory if needed.
307    if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(), entry)) {
308      metrics.recordUserAction('FolderShortcut.Navigate');
309      this.directoryModel_.changeDirectoryEntry(entry);
310    }
311  }.bind(this);
312
313  if (modelItem.isVolume) {
314    modelItem.volumeInfo.resolveDisplayRoot(
315        onEntryResolved,
316        function() {
317          // Error, the display root is not available. It may happen on Drive.
318          this.dataModel.onItemNotFoundError(modelItem);
319        }.bind(this));
320  } else if (modelItem.isShortcut) {
321    // For shortcuts we already have an Entry, but it has to be resolved again
322    // in case, it points to a non-existing directory.
323    var url = modelItem.entry.toURL();
324    webkitResolveLocalFileSystemURL(
325        url,
326        onEntryResolved,
327        function() {
328          // Error, the entry can't be re-resolved. It may happen for shortcuts
329          // which targets got removed after resolving the Entry during
330          // initialization.
331          this.dataModel.onItemNotFoundError(modelItem);
332        }.bind(this));
333  }
334};
335
336/**
337 * Handler before root item change.
338 * @param {Event} event The event.
339 * @private
340 */
341NavigationList.prototype.onBeforeSelectionChange_ = function(event) {
342  if (event.changes.length == 1 && !event.changes[0].selected)
343    event.preventDefault();
344};
345
346/**
347 * Handler for root item being clicked.
348 * @param {Event} event The event.
349 * @private
350 */
351NavigationList.prototype.onSelectionChange_ = function(event) {
352  var index = this.selectionModel.selectedIndex;
353  if (index < 0 || index > this.dataModel.length - 1)
354    return;
355
356  // If the selected model item is not changed actually, we don't change the
357  // current directory even if the selected index is changed.
358  var modelItem = this.dataModel.item(index);
359  if (modelItem === this.currentModelItem_)
360    return;
361
362  // Remembers the selected model item.
363  this.currentModelItem_ = modelItem;
364
365  // This handler is invoked even when the navigation list itself changes the
366  // selection. In such case, we shouldn't handle the event.
367  if (this.dontHandleSelectionEvent_)
368    return;
369
370  this.activateModelItem_(modelItem);
371};
372
373/**
374 * Invoked when the current directory is changed.
375 * @param {Event} event The event.
376 * @private
377 */
378NavigationList.prototype.onCurrentDirectoryChanged_ = function(event) {
379  this.selectBestMatchItem_();
380};
381
382/**
383 * Invoked when the content in the data model is changed.
384 * @param {Event} event The event.
385 * @private
386 */
387NavigationList.prototype.onListContentChanged_ = function(event) {
388  this.selectBestMatchItem_();
389};
390
391/**
392 * Synchronizes the volume list selection with the current directory, after
393 * it is changed outside of the volume list.
394 * @private
395 */
396NavigationList.prototype.selectBestMatchItem_ = function() {
397  var entry = this.directoryModel_.getCurrentDirEntry();
398  // It may happen that the current directory is not set yet, for update events.
399  if (!entry)
400    return;
401
402  // (1) Select the nearest shortcut directory.
403  var entryURL = entry.toURL();
404  var bestMatchIndex = -1;
405  var bestMatchSubStringLen = 0;
406  for (var i = 0; i < this.dataModel.length; i++) {
407    var modelItem = this.dataModel.item(i);
408    if (!modelItem.isShortcut)
409      continue;
410    var modelItemURL = modelItem.entry.toURL();
411    // Relying on URL's format is risky and should be avoided. However, there is
412    // no other way to quickly check if an entry is an ancestor of another one.
413    if (entryURL.indexOf(modelItemURL) === 0) {
414      if (bestMatchSubStringLen < modelItemURL.length) {
415        bestMatchIndex = i;
416        bestMatchSubStringLen = modelItemURL.length;
417      }
418    }
419  }
420  if (bestMatchIndex != -1) {
421    // Don't to invoke the handler of this instance, sets the guard.
422    this.dontHandleSelectionEvent_ = true;
423    this.selectionModel.selectedIndex = bestMatchIndex;
424    this.dontHandleSelectionEvent_ = false;
425    return;
426  }
427
428  // (2) Selects the volume of the current directory.
429  var volumeInfo = this.volumeManager_.getVolumeInfo(entry);
430  if (!volumeInfo)
431    return;
432  for (var i = 0; i < this.dataModel.length; i++) {
433    var modelItem = this.dataModel.item(i);
434    if (!modelItem.isVolume)
435      continue;
436    if (modelItem.volumeInfo === volumeInfo) {
437      // Don't to invoke the handler of this instance, sets the guard.
438      this.dontHandleSelectionEvent_ = true;
439      this.selectionModel.selectedIndex = i;
440      this.dontHandleSelectionEvent_ = false;
441      return;
442    }
443  }
444};
445