OLD | NEW |
| (Empty) |
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 volume list model. This model combines the 2 lists. | |
9 * @param {cr.ui.ArrayDataModel} volumesList The first list of the model. | |
10 * @param {cr.ui.ArrayDataModel} pinnedList The second list of the model. | |
11 * @constructor | |
12 * @extends {cr.EventTarget} | |
13 */ | |
14 function VolumeListModel(volumesList, pinnedList) { | |
15 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 | |
60 // 'splice' and 'sorted' events are not implemented, since they are not used | |
61 // in list.js. | |
62 } | |
63 | |
64 /** | |
65 * VolumeList inherits cr.EventTarget. | |
66 */ | |
67 VolumeListModel.prototype = { | |
68 __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 */ | |
77 VolumeListModel.prototype.item = function(index) { | |
78 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 /** | |
88 * Type of the item on the volume list. | |
89 * @enum {number} | |
90 */ | |
91 VolumeListModel.ItemType = { | |
92 ROOT: 1, | |
93 PINNED: 2 | |
94 }; | |
95 | |
96 /** | |
97 * Returns the type of the item at the given index. | |
98 * @param {number} index The index of the entry to get. | |
99 * @return {VolumeListModel.ItemType} The type of the item. | |
100 */ | |
101 VolumeListModel.prototype.getItemType = function(index) { | |
102 var offset = this.volumesList_.length; | |
103 return index < offset ? | |
104 VolumeListModel.ItemType.ROOT : VolumeListModel.ItemType.PINNED; | |
105 }; | |
106 | |
107 /** | |
108 * Returns the number of items in the model. | |
109 * @return {number} The length of the model. | |
110 * @private | |
111 */ | |
112 VolumeListModel.prototype.length_ = function() { | |
113 return this.volumesList_.length + this.pinnedList_.length; | |
114 }; | |
115 | |
116 /** | |
117 * Returns the first matching item. | |
118 * @param {Entry} item The entry to find. | |
119 * @param {number=} opt_fromIndex If provided, then the searching start at | |
120 * the {@code opt_fromIndex}. | |
121 * @return {number} The index of the first found element or -1 if not found. | |
122 */ | |
123 VolumeListModel.prototype.indexOf = function(item, opt_fromIndex) { | |
124 for (var i = opt_fromIndex || 0; i < this.length; i++) { | |
125 if (item === this.item(i)) | |
126 return i; | |
127 } | |
128 return -1; | |
129 }; | |
130 | |
131 /** | |
132 * A volume item. | |
133 * @constructor | |
134 * @extends {HTMLLIElement} | |
135 */ | |
136 var VolumeItem = cr.ui.define('li'); | |
137 | |
138 VolumeItem.prototype = { | |
139 __proto__: HTMLLIElement.prototype, | |
140 }; | |
141 | |
142 /** | |
143 * Decorate the item. | |
144 */ | |
145 VolumeItem.prototype.decorate = function() { | |
146 // decorate() may be called twice: from the constructor and from | |
147 // List.createItem(). This check prevents double-decorating. | |
148 if (this.className) | |
149 return; | |
150 | |
151 this.className = 'root-item'; | |
152 this.setAttribute('role', 'option'); | |
153 | |
154 this.iconDiv_ = cr.doc.createElement('div'); | |
155 this.iconDiv_.className = 'volume-icon'; | |
156 this.appendChild(this.iconDiv_); | |
157 | |
158 this.label_ = cr.doc.createElement('div'); | |
159 this.label_.className = 'root-label'; | |
160 this.appendChild(this.label_); | |
161 | |
162 cr.defineProperty(this, 'lead', cr.PropertyKind.BOOL_ATTR); | |
163 cr.defineProperty(this, 'selected', cr.PropertyKind.BOOL_ATTR); | |
164 }; | |
165 | |
166 /** | |
167 * Associate a path with this item. | |
168 * @param {string} path Path of this item. | |
169 */ | |
170 VolumeItem.prototype.setPath = function(path) { | |
171 if (this.path_) | |
172 console.warn('VolumeItem.setPath should be called only once.'); | |
173 | |
174 this.path_ = path; | |
175 | |
176 var rootType = PathUtil.getRootType(path); | |
177 | |
178 this.iconDiv_.setAttribute('volume-type-icon', rootType); | |
179 if (rootType === RootType.REMOVABLE) { | |
180 this.iconDiv_.setAttribute('volume-subtype', | |
181 VolumeManager.getInstance().getDeviceType(path)); | |
182 } | |
183 | |
184 this.label_.textContent = PathUtil.getFolderLabel(path); | |
185 | |
186 if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) { | |
187 this.eject_ = cr.doc.createElement('div'); | |
188 // Block other mouse handlers. | |
189 this.eject_.addEventListener( | |
190 'mouseup', function(e) { e.stopPropagation() }); | |
191 this.eject_.addEventListener( | |
192 'mousedown', function(e) { e.stopPropagation() }); | |
193 | |
194 this.eject_.className = 'root-eject'; | |
195 this.eject_.addEventListener('click', function(event) { | |
196 event.stopPropagation(); | |
197 cr.dispatchSimpleEvent(this, 'eject'); | |
198 }.bind(this)); | |
199 | |
200 this.appendChild(this.eject_); | |
201 } | |
202 }; | |
203 | |
204 /** | |
205 * Associate a context menu with this item. | |
206 * @param {cr.ui.Menu} menu Menu this item. | |
207 */ | |
208 VolumeItem.prototype.maybeSetContextMenu = function(menu) { | |
209 if (!this.path_) { | |
210 console.error( | |
211 'VolumeItem.maybeSetContextMenu must be called after setPath().'); | |
212 return; | |
213 } | |
214 | |
215 var isRoot = PathUtil.isRootPath(this.path_); | |
216 var rootType = PathUtil.getRootType(this.path_); | |
217 // The context menu is shown on the following items: | |
218 // - Removable and Archive volumes | |
219 // - Folder shortcuts | |
220 if (!isRoot || | |
221 (rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS)) | |
222 cr.ui.contextMenuHandler.setContextMenu(this, menu); | |
223 }; | |
224 | |
225 /** | |
226 * A volume list. | |
227 * @constructor | |
228 * @extends {cr.ui.List} | |
229 */ | |
230 function VolumeList() { | |
231 } | |
232 | |
233 /** | |
234 * VolumeList inherits cr.ui.List. | |
235 */ | |
236 VolumeList.prototype.__proto__ = cr.ui.List.prototype; | |
237 | |
238 /** | |
239 * @param {HTMLElement} el Element to be DirectoryItem. | |
240 * @param {DirectoryModel} directoryModel Current DirectoryModel. | |
241 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned | |
242 * folders. | |
243 */ | |
244 VolumeList.decorate = function(el, directoryModel, pinnedFolderModel) { | |
245 el.__proto__ = VolumeList.prototype; | |
246 el.decorate(directoryModel, pinnedFolderModel); | |
247 }; | |
248 | |
249 /** | |
250 * @param {DirectoryModel} directoryModel Current DirectoryModel. | |
251 * @param {cr.ui.ArrayDataModel} pinnedFolderModel Current model of the pinned | |
252 * folders. | |
253 */ | |
254 VolumeList.prototype.decorate = function(directoryModel, pinnedFolderModel) { | |
255 cr.ui.List.decorate(this); | |
256 this.__proto__ = VolumeList.prototype; | |
257 | |
258 this.directoryModel_ = directoryModel; | |
259 this.volumeManager_ = VolumeManager.getInstance(); | |
260 this.selectionModel = new cr.ui.ListSingleSelectionModel(); | |
261 | |
262 this.directoryModel_.addEventListener('directory-changed', | |
263 this.onCurrentDirectoryChanged_.bind(this)); | |
264 this.selectionModel.addEventListener( | |
265 'change', this.onSelectionChange_.bind(this)); | |
266 this.selectionModel.addEventListener( | |
267 'beforeChange', this.onBeforeSelectionChange_.bind(this)); | |
268 | |
269 this.scrollBar_ = new ScrollBar(); | |
270 this.scrollBar_.initialize(this.parentNode, this); | |
271 | |
272 // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox' | |
273 // role for better accessibility on ChromeOS. | |
274 this.setAttribute('role', 'listbox'); | |
275 | |
276 var self = this; | |
277 this.itemConstructor = function(path) { | |
278 return self.renderRoot_(path); | |
279 }; | |
280 | |
281 this.pinnedItemList_ = pinnedFolderModel; | |
282 | |
283 this.dataModel = | |
284 new VolumeListModel(this.directoryModel_.getRootsList(), | |
285 this.pinnedItemList_); | |
286 }; | |
287 | |
288 /** | |
289 * Creates an element of a volume. This method is called from cr.ui.List | |
290 * internally. | |
291 * @param {string} path Path of the directory to be rendered. | |
292 * @return {VolumeItem} Rendered element. | |
293 * @private | |
294 */ | |
295 VolumeList.prototype.renderRoot_ = function(path) { | |
296 var item = new VolumeItem(); | |
297 item.setPath(path); | |
298 | |
299 var handleClick = function() { | |
300 if (item.selected && path !== this.directoryModel_.getCurrentDirPath()) { | |
301 this.directoryModel_.changeDirectory(path); | |
302 } | |
303 }.bind(this); | |
304 item.addEventListener('click', handleClick); | |
305 | |
306 var handleEject = function() { | |
307 var unmountCommand = cr.doc.querySelector('command#unmount'); | |
308 // Let's make sure 'canExecute' state of the command is properly set for | |
309 // the root before executing it. | |
310 unmountCommand.canExecuteChange(item); | |
311 unmountCommand.execute(item); | |
312 }; | |
313 item.addEventListener('eject', handleEject); | |
314 // TODO(yoshiki): Check if the following touch handler is necessary or not. | |
315 // If unnecessary, remove it. | |
316 item.addEventListener(cr.ui.TouchHandler.EventType.TOUCH_START, handleClick); | |
317 | |
318 if (this.contextMenu_) | |
319 item.maybeSetContextMenu(this.contextMenu_); | |
320 | |
321 // If the current directory is already set. | |
322 if (this.currentVolume_ == path) { | |
323 setTimeout(function() { | |
324 this.selectedItem = path; | |
325 }.bind(this), 0); | |
326 } | |
327 return item; | |
328 }; | |
329 | |
330 /** | |
331 * Sets a context menu. Context menu is enabled only on archive and removable | |
332 * volumes as of now. | |
333 * | |
334 * @param {cr.ui.Menu} menu Context menu. | |
335 */ | |
336 VolumeList.prototype.setContextMenu = function(menu) { | |
337 this.contextMenu_ = menu; | |
338 | |
339 for (var i = 0; i < this.dataModel.length; i++) { | |
340 this.getListItemByIndex(i).maybeSetContextMenu(this.contextMenu_); | |
341 } | |
342 }; | |
343 | |
344 /** | |
345 * Selects the n-th volume from the list. | |
346 * @param {number} index Volume index. | |
347 * @return {boolean} True for success, otherwise false. | |
348 */ | |
349 VolumeList.prototype.selectByIndex = function(index) { | |
350 if (index < 0 || index > this.dataModel.length - 1) | |
351 return false; | |
352 | |
353 var newPath = this.dataModel.item(index); | |
354 if (!newPath) | |
355 return false; | |
356 | |
357 // Prevents double-moving to the current directory. | |
358 if (this.directoryModel_.getCurrentDirEntry().fullPath == newPath) | |
359 return false; | |
360 | |
361 this.directoryModel_.changeDirectory(newPath); | |
362 return true; | |
363 }; | |
364 | |
365 /** | |
366 * Handler before root item change. | |
367 * @param {Event} event The event. | |
368 * @private | |
369 */ | |
370 VolumeList.prototype.onBeforeSelectionChange_ = function(event) { | |
371 if (event.changes.length == 1 && !event.changes[0].selected) | |
372 event.preventDefault(); | |
373 }; | |
374 | |
375 /** | |
376 * Handler for root item being clicked. | |
377 * @param {Event} event The event. | |
378 * @private | |
379 */ | |
380 VolumeList.prototype.onSelectionChange_ = function(event) { | |
381 // This handler is invoked even when the volume list itself changes the | |
382 // selection. In such case, we shouldn't handle the event. | |
383 if (this.dontHandleSelectionEvent_) | |
384 return; | |
385 | |
386 this.selectByIndex(this.selectionModel.selectedIndex); | |
387 }; | |
388 | |
389 /** | |
390 * Invoked when the current directory is changed. | |
391 * @param {Event} event The event. | |
392 * @private | |
393 */ | |
394 VolumeList.prototype.onCurrentDirectoryChanged_ = function(event) { | |
395 var path = event.newDirEntry.fullPath || this.dataModel.getCurrentDirPath(); | |
396 var newRootPath = PathUtil.getRootPath(path); | |
397 | |
398 // Synchronizes the volume list selection with the current directory, after | |
399 // it is changed outside of the volume list. | |
400 | |
401 // (1) Select the nearest parent directory (including the pinned directories). | |
402 var bestMatchIndex = -1; | |
403 var bestMatchSubStringLen = 0; | |
404 for (var i = 0; i < this.dataModel.length; i++) { | |
405 var itemPath = this.dataModel.item(i); | |
406 if (path.indexOf(itemPath) == 0) { | |
407 if (bestMatchSubStringLen < itemPath.length) { | |
408 bestMatchIndex = i; | |
409 bestMatchSubStringLen = itemPath.length; | |
410 } | |
411 } | |
412 } | |
413 if (bestMatchIndex != -1) { | |
414 // Not to invoke the handler of this instance, sets the guard. | |
415 this.dontHandleSelectionEvent_ = true; | |
416 this.selectionModel.selectedIndex = bestMatchIndex; | |
417 this.dontHandleSelectionEvent_ = false; | |
418 return; | |
419 } | |
420 | |
421 // (2) Selects the volume of the current directory. | |
422 for (var i = 0; i < this.dataModel.length; i++) { | |
423 var itemPath = this.dataModel.item(i); | |
424 if (PathUtil.getRootPath(itemPath) == newRootPath) { | |
425 // Not to invoke the handler of this instance, sets the guard. | |
426 this.dontHandleSelectionEvent_ = true; | |
427 this.selectionModel.selectedIndex = i; | |
428 this.dontHandleSelectionEvent_ = false; | |
429 return; | |
430 } | |
431 } | |
432 }; | |
OLD | NEW |