- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / foreground / js / ui / navigation_list.js
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  */
12 var NavigationListItem = cr.ui.define('li');
13
14 NavigationListItem.prototype = {
15   __proto__: HTMLLIElement.prototype,
16   get modelItem() { return this.modelItem_; }
17 };
18
19 /**
20  * Decorate the item.
21  */
22 NavigationListItem.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  */
49 NavigationListItem.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 = PathUtil.getFolderLabel(modelItem.path);
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  */
86 NavigationListItem.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  */
108 function NavigationList() {
109 }
110
111 /**
112  * NavigationList inherits cr.ui.List.
113  */
114 NavigationList.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  */
150 NavigationList.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  */
159 NavigationList.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  */
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;
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  */
212 NavigationList.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  */
251 NavigationList.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  */
266 NavigationList.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  */
280 NavigationList.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  */
306 NavigationList.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  */
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_)
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  */
330 NavigationList.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  */
339 NavigationList.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  */
348 NavigationList.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 };