blob: a0e55f3cf93168d480692d07aa6f28fe7d762a03 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* A volume list model. This model combines the 2 lists.
* @param {cr.ui.ArrayDataModel} volumesList The first list of the model.
* @param {cr.ui.ArrayDataModel} pinnedList The second list of the model.
* @constructor
* @extends {cr.EventTarget}
*/
function VolumeListModel(volumesList, pinnedList) {
this.volumesList_ = volumesList;
this.pinnedList_ = pinnedList;
// Generates a combined 'permuted' event from an event of either list.
var permutedHandler = function(listNum, e) {
var permutedEvent = new Event('permuted');
var newPermutation = [];
var newLength;
if (listNum == 1) {
newLength = e.newLength + this.pinnedList_.length;
for (var i = 0; i < e.permutation.length; i++) {
newPermutation[i] = e.permutation[i];
}
for (var i = 0; i < this.pinnedList_.length; i++) {
newPermutation[i + e.permutation.length] = i + e.newLength;
}
} else {
var volumesLen = this.volumesList_.length;
newLength = e.newLength + volumesLen;
for (var i = 0; i < volumesLen; i++) {
newPermutation[i] = i;
}
for (var i = 0; i < e.permutation.length; i++) {
newPermutation[i + volumesLen] =
(e.permutation[i] !== -1) ? (e.permutation[i] + volumesLen) : -1;
}
}
permutedEvent.newLength = newLength;
permutedEvent.permutation = newPermutation;
this.dispatchEvent(permutedEvent);
};
this.volumesList_.addEventListener('permuted', permutedHandler.bind(this, 1));
this.pinnedList_.addEventListener('permuted', permutedHandler.bind(this, 2));
// Generates a combined 'change' event from an event of either list.
var changeHandler = function(listNum, e) {
var changeEvent = new Event('change');
changeEvent.index =
(listNum == 1) ? e.index : (e.index + this.volumesList_.length);
this.dispatchEvent(changeEvent);
};
this.volumesList_.addEventListener('change', changeHandler.bind(this, 1));
this.pinnedList_.addEventListener('change', changeHandler.bind(this, 2));
// 'splice' and 'sorted' events are not implemented, since they are not used
// in list.js.
}
/**
* VolumeList inherits cr.EventTarget.
*/
VolumeListModel.prototype = {
__proto__: cr.EventTarget.prototype,
get length() { return this.length_(); }
};
/**
* Returns the item at the given index.
* @param {number} index The index of the entry to get.
* @return {?string} The path at the given index.
*/
VolumeListModel.prototype.item = function(index) {
var offset = this.volumesList_.length;
if (index < offset) {
var entry = this.volumesList_.item(index);
return entry ? entry.fullPath : undefined;
} else {
return this.pinnedList_.item(index - offset);
}
};
/**
* Type of the item on the volume list.
* @enum {number}
*/
VolumeListModel.ItemType = {
ROOT: 1,
PINNED: 2
};
/**
* Returns the type of the item at the given index.
* @param {number} index The index of the entry to get.
* @return {VolumeListModel.ItemType} The type of the item.
*/
VolumeListModel.prototype.getItemType = function(index) {
var offset = this.volumesList_.length;
return index < offset ?
VolumeListModel.ItemType.ROOT : VolumeListModel.ItemType.PINNED;
};
/**
* Returns the number of items in the model.
* @return {number} The length of the model.
* @private
*/
VolumeListModel.prototype.length_ = function() {
return this.volumesList_.length + this.pinnedList_.length;
};
/**
* Returns the first matching item.
* @param {Entry} item The entry to find.
* @param {number=} opt_fromIndex If provided, then the searching start at
* the {@code opt_fromIndex}.
* @return {number} The index of the first found element or -1 if not found.
*/
VolumeListModel.prototype.indexOf = function(item, opt_fromIndex) {
for (var i = opt_fromIndex || 0; i < this.length; i++) {
if (item === this.item(i))
return i;
}
return -1;
};
/**
* A volume list.
*/
function VolumeList() {
}
/**
* VolumeList inherits cr.ui.List.
*/
VolumeList.prototype.__proto__ = cr.ui.List.prototype;
/**
* @param {HTMLElement} el Element to be DirectoryItem.
* @param {DirectoryModel} directoryModel Current DirectoryModel.
* @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned
* folders.
*/
VolumeList.decorate = function(el, directoryModel, pinnedFolderModel) {
el.__proto__ = VolumeList.prototype;
el.decorate(directoryModel, pinnedFolderModel);
};
/**
* @param {DirectoryModel} directoryModel Current DirectoryModel.
* @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned
* folders.
*/
VolumeList.prototype.decorate = function(directoryModel, pinnedFolderModel) {
cr.ui.List.decorate(this);
this.__proto__ = VolumeList.prototype;
this.directoryModel_ = directoryModel;
this.volumeManager_ = VolumeManager.getInstance();
this.selectionModel = new cr.ui.ListSingleSelectionModel();
this.directoryModel_.addEventListener('directory-changed',
this.onCurrentDirectoryChanged_.bind(this));
this.selectionModel.addEventListener(
'change', this.onSelectionChange_.bind(this));
this.selectionModel.addEventListener(
'beforeChange', this.onBeforeSelectionChange_.bind(this));
this.currentVolume_ = null;
this.scrollBar_ = new ScrollBar();
this.scrollBar_.initialize(this.parentNode, this);
// Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox'
// role for better accessibility on ChromeOS.
this.setAttribute('role', 'listbox');
var self = this;
this.itemConstructor = function(path) {
return self.renderRoot_(path);
};
this.pinnedItemList_ = pinnedFolderModel;
this.dataModel =
new VolumeListModel(this.directoryModel_.getRootsList(),
this.pinnedItemList_);
};
/**
* Creates an element of a volume. This method is called from cr.ui.List
* internally.
* @param {string} path Path of the directory to be rendered.
* @return {HTMLElement} Rendered element.
* @private
*/
VolumeList.prototype.renderRoot_ = function(path) {
var li = cr.doc.createElement('li');
li.className = 'root-item';
li.setAttribute('role', 'option');
var dm = this.directoryModel_;
var handleClick = function() {
if (li.selected && path !== dm.getCurrentDirPath()) {
this.directoryModel_.changeDirectory(path);
}
}.bind(this);
li.addEventListener('click', handleClick);
li.addEventListener(cr.ui.TouchHandler.EventType.TOUCH_START, handleClick);
var isRoot = PathUtil.isRootPath(path);
var rootType = PathUtil.getRootType(path);
var iconDiv = cr.doc.createElement('div');
iconDiv.className = 'volume-icon';
iconDiv.setAttribute('volume-type-icon', rootType);
if (rootType === RootType.REMOVABLE) {
iconDiv.setAttribute('volume-subtype',
this.volumeManager_.getDeviceType(path));
}
li.appendChild(iconDiv);
var div = cr.doc.createElement('div');
div.className = 'root-label';
div.textContent = PathUtil.getFolderLabel(path);
li.appendChild(div);
if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) {
var eject = cr.doc.createElement('div');
eject.className = 'root-eject';
eject.addEventListener('click', function(event) {
event.stopPropagation();
var unmountCommand = cr.doc.querySelector('command#unmount');
// Let's make sure 'canExecute' state of the command is properly set for
// the root before executing it.
unmountCommand.canExecuteChange(li);
unmountCommand.execute(li);
}.bind(this));
// Block other mouse handlers.
eject.addEventListener('mouseup', function(e) { e.stopPropagation() });
eject.addEventListener('mousedown', function(e) { e.stopPropagation() });
li.appendChild(eject);
}
if (this.contextMenu_ && (!isRoot ||
rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS))
cr.ui.contextMenuHandler.setContextMenu(li, this.contextMenu_);
cr.defineProperty(li, 'lead', cr.PropertyKind.BOOL_ATTR);
cr.defineProperty(li, 'selected', cr.PropertyKind.BOOL_ATTR);
// If the current directory is already set.
if (this.currentVolume_ == path) {
setTimeout(function() {
this.selectedItem = path;
}.bind(this), 0);
}
return li;
};
/**
* Sets a context menu. Context menu is enabled only on archive and removable
* volumes as of now.
*
* @param {cr.ui.Menu} menu Context menu.
*/
VolumeList.prototype.setContextMenu = function(menu) {
this.contextMenu_ = menu;
for (var i = 0; i < this.dataModel.length; i++) {
var path = this.dataModel.item(i);
var itemType = this.dataModel.getItemType(i);
var type = PathUtil.getRootType(path);
// Context menu is set only to archive and removable volumes.
if (itemType == VolumeListModel.ItemType.PINNED ||
type == RootType.ARCHIVE || type == RootType.REMOVABLE) {
cr.ui.contextMenuHandler.setContextMenu(this.getListItemByIndex(i),
this.contextMenu_);
}
}
};
/**
* Selects the n-th volume from the list.
* @param {number} index Volume index.
* @return {boolean} True for success, otherwise false.
*/
VolumeList.prototype.selectByIndex = function(index) {
if (index < 0 || index > this.dataModel.length - 1)
return false;
var newPath = this.dataModel.item(index);
if (!newPath || this.currentVolume_ == newPath)
return false;
this.currentVolume_ = newPath;
this.directoryModel_.changeDirectory(this.currentVolume_);
return true;
};
/**
* Handler before root item change.
* @param {Event} event The event.
* @private
*/
VolumeList.prototype.onBeforeSelectionChange_ = function(event) {
if (event.changes.length == 1 && !event.changes[0].selected)
event.preventDefault();
};
/**
* Handler for root item being clicked.
* @param {Event} event The event.
* @private
*/
VolumeList.prototype.onSelectionChange_ = function(event) {
// This handler is invoked even when the volume list itself changes the
// selection. In such case, we shouldn't handle the event.
if (this.dontHandleSelectionEvent_)
return;
this.selectByIndex(this.selectionModel.selectedIndex);
};
/**
* Invoked when the current directory is changed.
* @param {Event} event The event.
* @private
*/
VolumeList.prototype.onCurrentDirectoryChanged_ = function(event) {
var path = event.newDirEntry.fullPath || this.dataModel.getCurrentDirPath();
var newRootPath = PathUtil.getRootPath(path);
// Sets |this.currentVolume_| in advance to prevent |onSelectionChange_()|
// from calling |DirectoryModel.ChangeDirectory()| again.
this.currentVolume_ = newRootPath;
// Synchronizes the volume list selection with the current directory, after
// it is changed outside of the volume list.
// (1) Select the nearest parent directory (including the pinned directories).
var bestMatchIndex = -1;
var bestMatchSubStringLen = 0;
for (var i = 0; i < this.dataModel.length; i++) {
var itemPath = this.dataModel.item(i);
if (path.indexOf(itemPath) == 0) {
if (bestMatchSubStringLen < itemPath.length) {
bestMatchIndex = i;
bestMatchSubStringLen = itemPath.length;
}
}
}
if (bestMatchIndex != -1) {
// Not to invoke the handler of this instance, sets the guard.
this.dontHandleSelectionEvent_ = true;
this.selectionModel.selectedIndex = bestMatchIndex;
this.dontHandleSelectionEvent_ = false;
return;
}
// (2) Selects the volume of the current directory.
for (var i = 0; i < this.dataModel.length; i++) {
var itemPath = this.dataModel.item(i);
if (PathUtil.getRootPath(itemPath) == newRootPath) {
// Not to invoke the handler of this instance, sets the guard.
this.dontHandleSelectionEvent_ = true;
this.selectionModel.selectedIndex = i;
this.dontHandleSelectionEvent_ = false;
return;
}
}
};