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 * Base item of NavigationListModel. Should not be created directly.
9 * @param {string} label Label.
10 * @constructor
11 */
12function NavigationModelItem(label) {
13  this.label_ = label;
14}
15
16NavigationModelItem.prototype = {
17  get label() { return this.label_; }
18};
19
20/**
21 * Check whether given two model items are same.
22 * @param {NavigationModelItem} item1 The first item to be compared.
23 * @param {NavigationModelItem} item2 The second item to be compared.
24 * @return {boolean} True if given two model items are same.
25 */
26NavigationModelItem.isSame = function(item1, item2) {
27  if (item1.isVolume != item2.isVolume)
28    return false;
29
30  if (item1.isVolume)
31    return item1.volumeInfo === item2.volumeInfo;
32  else
33    return util.isSameEntry(item1.entry, item2.entry);
34};
35
36/**
37 * Item of NavigationListModel for shortcuts.
38 *
39 * @param {string} label Label.
40 * @param {!DirectoryEntry} entry Entry. Cannot be null.
41 * @constructor
42 * @extends {NavigationModelItem}
43 */
44function NavigationModelShortcutItem(label, entry) {
45  NavigationModelItem.call(this, label);
46  this.entry_ = entry;
47  Object.freeze(this);
48}
49
50NavigationModelShortcutItem.prototype = {
51  __proto__: NavigationModelItem.prototype,
52  get entry() { return this.entry_; },
53  get isVolume() { return false; },
54  get isShortcut() { return true; }
55};
56
57/**
58 * Item of NavigationListModel for volumes.
59 *
60 * @param {string} label Label.
61 * @param {!VolumeInfo} volumeInfo Volume info for the volume. Cannot be null.
62 * @constructor
63 * @extends {NavigationModelItem}
64 */
65function NavigationModelVolumeItem(label, volumeInfo) {
66  NavigationModelItem.call(this, label);
67  this.volumeInfo_ = volumeInfo;
68  // Start resolving the display root because it is used
69  // for determining executability of commands.
70  this.volumeInfo_.resolveDisplayRoot(
71      function() {}, function() {});
72  Object.freeze(this);
73}
74
75NavigationModelVolumeItem.prototype = {
76  __proto__: NavigationModelItem.prototype,
77  get volumeInfo() { return this.volumeInfo_; },
78  get isVolume() { return true; },
79  get isShortcut() { return false; }
80};
81
82/**
83 * A navigation list model. This model combines the 2 lists.
84 * @param {VolumeManagerWrapper} volumeManager VolumeManagerWrapper instance.
85 * @param {cr.ui.ArrayDataModel} shortcutListModel The list of folder shortcut.
86 * @constructor
87 * @extends {cr.EventTarget}
88 */
89function NavigationListModel(volumeManager, shortcutListModel) {
90  cr.EventTarget.call(this);
91
92  this.volumeManager_ = volumeManager;
93  this.shortcutListModel_ = shortcutListModel;
94
95  var volumeInfoToModelItem = function(volumeInfo) {
96    return new NavigationModelVolumeItem(
97        volumeInfo.label,
98        volumeInfo);
99  }.bind(this);
100
101  var entryToModelItem = function(entry) {
102    var item = new NavigationModelShortcutItem(
103        entry.name,
104        entry);
105    return item;
106  }.bind(this);
107
108  /**
109   * Type of updated list.
110   * @enum {number}
111   * @const
112   */
113  var ListType = {
114    VOLUME_LIST: 1,
115    SHORTCUT_LIST: 2
116  };
117  Object.freeze(ListType);
118
119  // Generates this.volumeList_ and this.shortcutList_ from the models.
120  this.volumeList_ =
121      this.volumeManager_.volumeInfoList.slice().map(volumeInfoToModelItem);
122
123  this.shortcutList_ = [];
124  for (var i = 0; i < this.shortcutListModel_.length; i++) {
125    var shortcutEntry = this.shortcutListModel_.item(i);
126    var volumeInfo = this.volumeManager_.getVolumeInfo(shortcutEntry);
127    this.shortcutList_.push(entryToModelItem(shortcutEntry));
128  }
129
130  // Generates a combined 'permuted' event from an event of either list.
131  var permutedHandler = function(listType, event) {
132    var permutation;
133
134    // Build the volumeList.
135    if (listType == ListType.VOLUME_LIST) {
136      // The volume is mounted or unmounted.
137      var newList = [];
138
139      // Use the old instances if they just move.
140      for (var i = 0; i < event.permutation.length; i++) {
141        if (event.permutation[i] >= 0)
142          newList[event.permutation[i]] = this.volumeList_[i];
143      }
144
145      // Create missing instances.
146      for (var i = 0; i < event.newLength; i++) {
147        if (!newList[i]) {
148          newList[i] = volumeInfoToModelItem(
149              this.volumeManager_.volumeInfoList.item(i));
150        }
151      }
152      this.volumeList_ = newList;
153
154      permutation = event.permutation.slice();
155
156      // shortcutList part has not been changed, so the permutation should be
157      // just identity mapping with a shift.
158      for (var i = 0; i < this.shortcutList_.length; i++) {
159        permutation.push(i + this.volumeList_.length);
160      }
161    } else {
162      // Build the shortcutList.
163
164      // volumeList part has not been changed, so the permutation should be
165      // identity mapping.
166
167      permutation = [];
168      for (var i = 0; i < this.volumeList_.length; i++) {
169        permutation[i] = i;
170      }
171
172      var modelIndex = 0;
173      var oldListIndex = 0;
174      var newList = [];
175      while (modelIndex < this.shortcutListModel_.length &&
176             oldListIndex < this.shortcutList_.length) {
177        var shortcutEntry = this.shortcutListModel_.item(modelIndex);
178        var cmp = this.shortcutListModel_.compare(
179            shortcutEntry, this.shortcutList_[oldListIndex].entry);
180        if (cmp > 0) {
181          // The shortcut at shortcutList_[oldListIndex] is removed.
182          permutation.push(-1);
183          oldListIndex++;
184          continue;
185        }
186
187        if (cmp === 0) {
188          // Reuse the old instance.
189          permutation.push(newList.length + this.volumeList_.length);
190          newList.push(this.shortcutList_[oldListIndex]);
191          oldListIndex++;
192        } else {
193          // We needs to create a new instance for the shortcut entry.
194          newList.push(entryToModelItem(shortcutEntry));
195        }
196        modelIndex++;
197      }
198
199      // Add remaining (new) shortcuts if necessary.
200      for (; modelIndex < this.shortcutListModel_.length; modelIndex++) {
201        var shortcutEntry = this.shortcutListModel_.item(modelIndex);
202        newList.push(entryToModelItem(shortcutEntry));
203      }
204
205      // Fill remaining permutation if necessary.
206      for (; oldListIndex < this.shortcutList_.length; oldListIndex++)
207        permutation.push(-1);
208
209      this.shortcutList_ = newList;
210    }
211
212    // Dispatch permuted event.
213    var permutedEvent = new Event('permuted');
214    permutedEvent.newLength =
215        this.volumeList_.length + this.shortcutList_.length;
216    permutedEvent.permutation = permutation;
217    this.dispatchEvent(permutedEvent);
218  };
219
220  this.volumeManager_.volumeInfoList.addEventListener(
221      'permuted', permutedHandler.bind(this, ListType.VOLUME_LIST));
222  this.shortcutListModel_.addEventListener(
223      'permuted', permutedHandler.bind(this, ListType.SHORTCUT_LIST));
224
225  // 'change' event is just ignored, because it is not fired neither in
226  // the folder shortcut list nor in the volume info list.
227  // 'splice' and 'sorted' events are not implemented, since they are not used
228  // in list.js.
229}
230
231/**
232 * NavigationList inherits cr.EventTarget.
233 */
234NavigationListModel.prototype = {
235  __proto__: cr.EventTarget.prototype,
236  get length() { return this.length_(); },
237  get folderShortcutList() { return this.shortcutList_; }
238};
239
240/**
241 * Returns the item at the given index.
242 * @param {number} index The index of the entry to get.
243 * @return {NavigationModelItem} The item at the given index.
244 */
245NavigationListModel.prototype.item = function(index) {
246  var offset = this.volumeList_.length;
247  if (index < offset)
248    return this.volumeList_[index];
249  return this.shortcutList_[index - offset];
250};
251
252/**
253 * Returns the number of items in the model.
254 * @return {number} The length of the model.
255 * @private
256 */
257NavigationListModel.prototype.length_ = function() {
258  return this.volumeList_.length + this.shortcutList_.length;
259};
260
261/**
262 * Returns the first matching item.
263 * @param {NavigationModelItem} modelItem The entry to find.
264 * @param {number=} opt_fromIndex If provided, then the searching start at
265 *     the {@code opt_fromIndex}.
266 * @return {number} The index of the first found element or -1 if not found.
267 */
268NavigationListModel.prototype.indexOf = function(modelItem, opt_fromIndex) {
269  for (var i = opt_fromIndex || 0; i < this.length; i++) {
270    if (modelItem === this.item(i))
271      return i;
272  }
273  return -1;
274};
275
276/**
277 * Called externally when one of the items is not found on the filesystem.
278 * @param {NavigationModelItem} modelItem The entry which is not found.
279 */
280NavigationListModel.prototype.onItemNotFoundError = function(modelItem) {
281  if (modelItem.isVolume) {
282    // TODO(mtomasz, yoshiki): Implement when needed.
283    return;
284  }
285  if (modelItem.isShortcut) {
286    // For shortcuts, lets the shortcut model handle this situation.
287    this.shortcutListModel_.onItemNotFoundError(modelItem.entry);
288  }
289};
290