blob: 476fe6dd2da571aed3226e79e5aa061526aa0f69 [file] [log] [blame]
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01001// 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/**
Ben Murdoch32409262013-08-07 11:04:47 +01008 * A navigation list model. This model combines the 2 lists.
Ben Murdoch7dbb3d52013-07-17 14:55:54 +01009 * @param {cr.ui.ArrayDataModel} volumesList The first list of the model.
10 * @param {cr.ui.ArrayDataModel} pinnedList The second list of the model.
Ben Murdochca12bfa2013-07-23 11:17:05 +010011 * @constructor
12 * @extends {cr.EventTarget}
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010013 */
Ben Murdoch32409262013-08-07 11:04:47 +010014function NavigationListModel(volumesList, pinnedList) {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010015 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
Ben Murdochca12bfa2013-07-23 11:17:05 +010060 // 'splice' and 'sorted' events are not implemented, since they are not used
61 // in list.js.
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010062}
63
64/**
Ben Murdoch32409262013-08-07 11:04:47 +010065 * NavigationList inherits cr.EventTarget.
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010066 */
Ben Murdoch32409262013-08-07 11:04:47 +010067NavigationListModel.prototype = {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010068 __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 */
Ben Murdoch32409262013-08-07 11:04:47 +010077NavigationListModel.prototype.item = function(index) {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010078 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/**
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010088 * Returns the number of items in the model.
89 * @return {number} The length of the model.
90 * @private
91 */
Ben Murdoch32409262013-08-07 11:04:47 +010092NavigationListModel.prototype.length_ = function() {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +010093 return this.volumesList_.length + this.pinnedList_.length;
94};
95
96/**
97 * Returns the first matching item.
98 * @param {Entry} item The entry to find.
99 * @param {number=} opt_fromIndex If provided, then the searching start at
100 * the {@code opt_fromIndex}.
101 * @return {number} The index of the first found element or -1 if not found.
102 */
Ben Murdoch32409262013-08-07 11:04:47 +0100103NavigationListModel.prototype.indexOf = function(item, opt_fromIndex) {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100104 for (var i = opt_fromIndex || 0; i < this.length; i++) {
105 if (item === this.item(i))
106 return i;
107 }
108 return -1;
109};
110
111/**
Ben Murdoch32409262013-08-07 11:04:47 +0100112 * A navigation list item.
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100113 * @constructor
114 * @extends {HTMLLIElement}
115 */
Ben Murdoch32409262013-08-07 11:04:47 +0100116var NavigationListItem = cr.ui.define('li');
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100117
Ben Murdoch32409262013-08-07 11:04:47 +0100118NavigationListItem.prototype = {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100119 __proto__: HTMLLIElement.prototype,
Ben Murdoch2385ea32013-08-06 11:01:04 +0100120};
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100121
Ben Murdoch2385ea32013-08-06 11:01:04 +0100122/**
123 * Decorate the item.
124 */
Ben Murdoch32409262013-08-07 11:04:47 +0100125NavigationListItem.prototype.decorate = function() {
Ben Murdoch2385ea32013-08-06 11:01:04 +0100126 // decorate() may be called twice: from the constructor and from
127 // List.createItem(). This check prevents double-decorating.
128 if (this.className)
129 return;
130
131 this.className = 'root-item';
132 this.setAttribute('role', 'option');
133
134 this.iconDiv_ = cr.doc.createElement('div');
135 this.iconDiv_.className = 'volume-icon';
136 this.appendChild(this.iconDiv_);
137
138 this.label_ = cr.doc.createElement('div');
139 this.label_.className = 'root-label';
140 this.appendChild(this.label_);
141
142 cr.defineProperty(this, 'lead', cr.PropertyKind.BOOL_ATTR);
143 cr.defineProperty(this, 'selected', cr.PropertyKind.BOOL_ATTR);
144};
145
146/**
147 * Associate a path with this item.
148 * @param {string} path Path of this item.
149 */
Ben Murdoch32409262013-08-07 11:04:47 +0100150NavigationListItem.prototype.setPath = function(path) {
Ben Murdoch2385ea32013-08-06 11:01:04 +0100151 if (this.path_)
Ben Murdoch32409262013-08-07 11:04:47 +0100152 console.warn('NavigationListItem.setPath should be called only once.');
Ben Murdoch2385ea32013-08-06 11:01:04 +0100153
154 this.path_ = path;
155
156 var rootType = PathUtil.getRootType(path);
157
158 this.iconDiv_.setAttribute('volume-type-icon', rootType);
159 if (rootType === RootType.REMOVABLE) {
160 this.iconDiv_.setAttribute('volume-subtype',
161 VolumeManager.getInstance().getDeviceType(path));
162 }
163
164 this.label_.textContent = PathUtil.getFolderLabel(path);
165
166 if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) {
167 this.eject_ = cr.doc.createElement('div');
168 // Block other mouse handlers.
169 this.eject_.addEventListener(
170 'mouseup', function(e) { e.stopPropagation() });
171 this.eject_.addEventListener(
172 'mousedown', function(e) { e.stopPropagation() });
173
174 this.eject_.className = 'root-eject';
175 this.eject_.addEventListener('click', function(event) {
176 event.stopPropagation();
177 cr.dispatchSimpleEvent(this, 'eject');
178 }.bind(this));
179
180 this.appendChild(this.eject_);
181 }
182};
183
184/**
185 * Associate a context menu with this item.
186 * @param {cr.ui.Menu} menu Menu this item.
187 */
Ben Murdoch32409262013-08-07 11:04:47 +0100188NavigationListItem.prototype.maybeSetContextMenu = function(menu) {
Ben Murdoch2385ea32013-08-06 11:01:04 +0100189 if (!this.path_) {
Ben Murdoch32409262013-08-07 11:04:47 +0100190 console.error('NavigationListItem.maybeSetContextMenu must be called ' +
191 'after setPath().');
Ben Murdoch2385ea32013-08-06 11:01:04 +0100192 return;
193 }
194
195 var isRoot = PathUtil.isRootPath(this.path_);
196 var rootType = PathUtil.getRootType(this.path_);
197 // The context menu is shown on the following items:
198 // - Removable and Archive volumes
199 // - Folder shortcuts
200 if (!isRoot ||
201 (rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS))
202 cr.ui.contextMenuHandler.setContextMenu(this, menu);
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100203};
204
205/**
Ben Murdoch32409262013-08-07 11:04:47 +0100206 * A navigation list.
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100207 * @constructor
208 * @extends {cr.ui.List}
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100209 */
Ben Murdoch32409262013-08-07 11:04:47 +0100210function NavigationList() {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100211}
212
213/**
Ben Murdoch32409262013-08-07 11:04:47 +0100214 * NavigationList inherits cr.ui.List.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100215 */
Ben Murdoch32409262013-08-07 11:04:47 +0100216NavigationList.prototype.__proto__ = cr.ui.List.prototype;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100217
218/**
219 * @param {HTMLElement} el Element to be DirectoryItem.
220 * @param {DirectoryModel} directoryModel Current DirectoryModel.
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100221 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned
222 * folders.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100223 */
Ben Murdoch32409262013-08-07 11:04:47 +0100224NavigationList.decorate = function(el, directoryModel, pinnedFolderModel) {
225 el.__proto__ = NavigationList.prototype;
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100226 el.decorate(directoryModel, pinnedFolderModel);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100227};
228
229/**
230 * @param {DirectoryModel} directoryModel Current DirectoryModel.
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100231 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned
232 * folders.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100233 */
Ben Murdoch32409262013-08-07 11:04:47 +0100234NavigationList.prototype.decorate =
235 function(directoryModel, pinnedFolderModel) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100236 cr.ui.List.decorate(this);
Ben Murdoch32409262013-08-07 11:04:47 +0100237 this.__proto__ = NavigationList.prototype;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100238
239 this.directoryModel_ = directoryModel;
240 this.volumeManager_ = VolumeManager.getInstance();
241 this.selectionModel = new cr.ui.ListSingleSelectionModel();
242
243 this.directoryModel_.addEventListener('directory-changed',
244 this.onCurrentDirectoryChanged_.bind(this));
245 this.selectionModel.addEventListener(
246 'change', this.onSelectionChange_.bind(this));
247 this.selectionModel.addEventListener(
248 'beforeChange', this.onBeforeSelectionChange_.bind(this));
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100249
250 this.scrollBar_ = new ScrollBar();
251 this.scrollBar_.initialize(this.parentNode, this);
252
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100253 // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox'
254 // role for better accessibility on ChromeOS.
255 this.setAttribute('role', 'listbox');
256
257 var self = this;
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100258 this.itemConstructor = function(path) {
259 return self.renderRoot_(path);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100260 };
261
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100262 this.pinnedItemList_ = pinnedFolderModel;
263
264 this.dataModel =
Ben Murdoch32409262013-08-07 11:04:47 +0100265 new NavigationListModel(this.directoryModel_.getRootsList(),
266 this.pinnedItemList_);
267 this.dataModel.addEventListener(
268 'change', this.onDataModelChanged_.bind(this));
269 this.dataModel.addEventListener(
270 'permuted', this.onDataModelChanged_.bind(this));
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100271};
272
273/**
Ben Murdoch32409262013-08-07 11:04:47 +0100274 * Creates an element of a navigation list. This method is called from
275 * cr.ui.List internally.
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100276 * @param {string} path Path of the directory to be rendered.
Ben Murdoch32409262013-08-07 11:04:47 +0100277 * @return {NavigationListItem} Rendered element.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100278 * @private
279 */
Ben Murdoch32409262013-08-07 11:04:47 +0100280NavigationList.prototype.renderRoot_ = function(path) {
281 var item = new NavigationListItem();
Ben Murdoch2385ea32013-08-06 11:01:04 +0100282 item.setPath(path);
283
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100284 var handleClick = function() {
Ben Murdoch2385ea32013-08-06 11:01:04 +0100285 if (item.selected && path !== this.directoryModel_.getCurrentDirPath()) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100286 this.directoryModel_.changeDirectory(path);
287 }
288 }.bind(this);
Ben Murdoch2385ea32013-08-06 11:01:04 +0100289 item.addEventListener('click', handleClick);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100290
Ben Murdoch2385ea32013-08-06 11:01:04 +0100291 var handleEject = function() {
292 var unmountCommand = cr.doc.querySelector('command#unmount');
293 // Let's make sure 'canExecute' state of the command is properly set for
294 // the root before executing it.
295 unmountCommand.canExecuteChange(item);
296 unmountCommand.execute(item);
297 };
298 item.addEventListener('eject', handleEject);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100299
Ben Murdoch2385ea32013-08-06 11:01:04 +0100300 if (this.contextMenu_)
301 item.maybeSetContextMenu(this.contextMenu_);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100302
Ben Murdoch2385ea32013-08-06 11:01:04 +0100303 return item;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100304};
305
306/**
307 * Sets a context menu. Context menu is enabled only on archive and removable
308 * volumes as of now.
309 *
310 * @param {cr.ui.Menu} menu Context menu.
311 */
Ben Murdoch32409262013-08-07 11:04:47 +0100312NavigationList.prototype.setContextMenu = function(menu) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100313 this.contextMenu_ = menu;
314
315 for (var i = 0; i < this.dataModel.length; i++) {
Ben Murdoch2385ea32013-08-06 11:01:04 +0100316 this.getListItemByIndex(i).maybeSetContextMenu(this.contextMenu_);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100317 }
318};
319
320/**
Ben Murdoch32409262013-08-07 11:04:47 +0100321 * Selects the n-th item from the list.
322 * @param {number} index Item index.
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100323 * @return {boolean} True for success, otherwise false.
324 */
Ben Murdoch32409262013-08-07 11:04:47 +0100325NavigationList.prototype.selectByIndex = function(index) {
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100326 if (index < 0 || index > this.dataModel.length - 1)
327 return false;
328
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100329 var newPath = this.dataModel.item(index);
Ben Murdoch2385ea32013-08-06 11:01:04 +0100330 if (!newPath)
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100331 return false;
332
Ben Murdoch2385ea32013-08-06 11:01:04 +0100333 // Prevents double-moving to the current directory.
334 if (this.directoryModel_.getCurrentDirEntry().fullPath == newPath)
335 return false;
336
337 this.directoryModel_.changeDirectory(newPath);
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100338 return true;
339};
340
341/**
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100342 * Handler before root item change.
343 * @param {Event} event The event.
344 * @private
345 */
Ben Murdoch32409262013-08-07 11:04:47 +0100346NavigationList.prototype.onBeforeSelectionChange_ = function(event) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100347 if (event.changes.length == 1 && !event.changes[0].selected)
348 event.preventDefault();
349};
350
351/**
352 * Handler for root item being clicked.
353 * @param {Event} event The event.
354 * @private
355 */
Ben Murdoch32409262013-08-07 11:04:47 +0100356NavigationList.prototype.onSelectionChange_ = function(event) {
357 // This handler is invoked even when the navigation list itself changes the
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100358 // selection. In such case, we shouldn't handle the event.
359 if (this.dontHandleSelectionEvent_)
360 return;
361
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100362 this.selectByIndex(this.selectionModel.selectedIndex);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100363};
364
365/**
366 * Invoked when the current directory is changed.
367 * @param {Event} event The event.
368 * @private
369 */
Ben Murdoch32409262013-08-07 11:04:47 +0100370NavigationList.prototype.onCurrentDirectoryChanged_ = function(event) {
371 this.selectBestMatchItem_();
372};
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100373
Ben Murdoch32409262013-08-07 11:04:47 +0100374/**
375 * Invoked when the content in the data model is changed.
376 * @param {Event} event The event.
377 * @private
378 */
379NavigationList.prototype.onDataModelChanged_ = function(event) {
380 this.selectBestMatchItem_();
381};
382
383/**
384 * Synchronizes the volume list selection with the current directory, after
385 * it is changed outside of the volume list.
386 * @private
387 */
388NavigationList.prototype.selectBestMatchItem_ = function() {
389 var entry = this.directoryModel_.getCurrentDirEntry();
390 var path = entry && entry.fullPath;
391 if (!path)
392 return;
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100393
394 // (1) Select the nearest parent directory (including the pinned directories).
395 var bestMatchIndex = -1;
396 var bestMatchSubStringLen = 0;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100397 for (var i = 0; i < this.dataModel.length; i++) {
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100398 var itemPath = this.dataModel.item(i);
399 if (path.indexOf(itemPath) == 0) {
400 if (bestMatchSubStringLen < itemPath.length) {
401 bestMatchIndex = i;
402 bestMatchSubStringLen = itemPath.length;
403 }
404 }
405 }
406 if (bestMatchIndex != -1) {
407 // Not to invoke the handler of this instance, sets the guard.
408 this.dontHandleSelectionEvent_ = true;
409 this.selectionModel.selectedIndex = bestMatchIndex;
410 this.dontHandleSelectionEvent_ = false;
411 return;
412 }
413
414 // (2) Selects the volume of the current directory.
Ben Murdoch32409262013-08-07 11:04:47 +0100415 var newRootPath = PathUtil.getRootPath(path);
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100416 for (var i = 0; i < this.dataModel.length; i++) {
417 var itemPath = this.dataModel.item(i);
418 if (PathUtil.getRootPath(itemPath) == newRootPath) {
419 // Not to invoke the handler of this instance, sets the guard.
420 this.dontHandleSelectionEvent_ = true;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100421 this.selectionModel.selectedIndex = i;
Ben Murdoch7dbb3d52013-07-17 14:55:54 +0100422 this.dontHandleSelectionEvent_ = false;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100423 return;
424 }
425 }
426};