1// Copyright 2013 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 * @extends {cr.EventTarget}
9 * @param {HTMLDivElement} div Div container for breadcrumbs.
10 * @param {MetadataCache} metadataCache To retrieve metadata.
11 * @param {VolumeManagerWrapper} volumeManager Volume manager.
12 * @constructor
13 */
14function BreadcrumbsController(div, metadataCache, volumeManager) {
15  this.bc_ = div;
16  this.metadataCache_ = metadataCache;
17  this.volumeManager_ = volumeManager;
18  this.entry_ = null;
19
20  /**
21   * Sequence value to skip requests that are out of date.
22   * @type {number}
23   * @private
24   */
25  this.showSequence_ = 0;
26
27  // Register events and seql the object.
28  div.addEventListener('click', this.onClick_.bind(this));
29}
30
31/**
32 * Extends cr.EventTarget.
33 */
34BreadcrumbsController.prototype.__proto__ = cr.EventTarget.prototype;
35
36/**
37 * Shows breadcrumbs.
38 *
39 * @param {Entry} entry Target entry.
40 */
41BreadcrumbsController.prototype.show = function(entry) {
42  if (entry === this.entry_)
43    return;
44
45  this.entry_ = entry;
46  this.showSequence_++;
47
48  var queue = new AsyncUtil.Queue();
49  var entries = [];
50  var error = false;
51
52  // Obtain entries from the target entry to the root.
53  var resolveParent = function(currentEntry, previousEntry, callback) {
54    var entryLocationInfo = this.volumeManager_.getLocationInfo(currentEntry);
55    if (!entryLocationInfo) {
56      error = true;
57      callback();
58      return;
59    }
60
61    if (entryLocationInfo.isRootEntry &&
62        entryLocationInfo.rootType ===
63            VolumeManagerCommon.RootType.DRIVE_OTHER) {
64      this.metadataCache_.getOne(previousEntry, 'external', function(result) {
65        if (result && result.sharedWithMe) {
66          // Adds the shared-with-me entry instead.
67          var driveVolumeInfo = entryLocationInfo.volumeInfo;
68          var sharedWithMeEntry =
69              driveVolumeInfo.fakeEntries[
70                  VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME];
71          if (sharedWithMeEntry)
72            entries.unshift(sharedWithMeEntry);
73          else
74            error = true;
75        } else {
76          entries.unshift(currentEntry);
77        }
78        // Finishes traversal since the current is root.
79        callback();
80      });
81      return;
82    }
83
84    entries.unshift(currentEntry);
85    if (!entryLocationInfo.isRootEntry) {
86      currentEntry.getParent(function(parentEntry) {
87        resolveParent(parentEntry, currentEntry, callback);
88      }.bind(this), function() {
89        error = true;
90        callback();
91      });
92    } else {
93      callback();
94    }
95  }.bind(this);
96
97  queue.run(resolveParent.bind(this, entry, null));
98
99  queue.run(function(callback) {
100    // If an error occurred, just skip.
101    if (error) {
102      callback();
103      return;
104    }
105
106    // If the path is not under the drive other root, it is not needed to
107    // override root type.
108    var locationInfo = this.volumeManager_.getLocationInfo(entry);
109    if (!locationInfo)
110      error = true;
111
112    callback();
113  }.bind(this));
114
115  // Update DOM element.
116  queue.run(function(sequence, callback) {
117    // Check the sequence number to skip requests that are out of date.
118    if (this.showSequence_ === sequence) {
119      this.bc_.hidden = false;
120      this.bc_.textContent = '';
121      if (!error)
122        this.updateInternal_(entries);
123    }
124    callback();
125  }.bind(this, this.showSequence_));
126};
127
128/**
129 * Updates the breadcrumb display.
130 * @param {Array.<Entry>} entries Entries on the target path.
131 * @private
132 */
133BreadcrumbsController.prototype.updateInternal_ = function(entries) {
134  // Make elements.
135  var doc = this.bc_.ownerDocument;
136  for (var i = 0; i < entries.length; i++) {
137    // Add a component.
138    var entry = entries[i];
139    var div = doc.createElement('div');
140    div.className = 'breadcrumb-path entry-name';
141    div.textContent = util.getEntryLabel(this.volumeManager_, entry);
142    div.entry = entry;
143    this.bc_.appendChild(div);
144
145    // If this is the last component, break here.
146    if (i === entries.length - 1) {
147      div.classList.add('breadcrumb-last');
148      break;
149    }
150
151    // Add a separator.
152    var separator = doc.createElement('div');
153    separator.className = 'separator';
154    this.bc_.appendChild(separator);
155  }
156
157  this.truncate();
158};
159
160/**
161 * Updates breadcrumbs widths in order to truncate it properly.
162 */
163BreadcrumbsController.prototype.truncate = function() {
164  if (!this.bc_.firstChild)
165    return;
166
167  // Assume style.width == clientWidth (items have no margins or paddings).
168
169  for (var item = this.bc_.firstChild; item; item = item.nextSibling) {
170    item.removeAttribute('style');
171    item.removeAttribute('collapsed');
172  }
173
174  var containerWidth = this.bc_.clientWidth;
175
176  var pathWidth = 0;
177  var currentWidth = 0;
178  var lastSeparator;
179  for (var item = this.bc_.firstChild; item; item = item.nextSibling) {
180    if (item.className == 'separator') {
181      pathWidth += currentWidth;
182      currentWidth = item.clientWidth;
183      lastSeparator = item;
184    } else {
185      currentWidth += item.clientWidth;
186    }
187  }
188  if (pathWidth + currentWidth <= containerWidth)
189    return;
190  if (!lastSeparator) {
191    this.bc_.lastChild.style.width =
192        Math.min(currentWidth, containerWidth) + 'px';
193    return;
194  }
195  var lastCrumbSeparatorWidth = lastSeparator.clientWidth;
196  // Current directory name may occupy up to 70% of space or even more if the
197  // path is short.
198  var maxPathWidth = Math.max(Math.round(containerWidth * 0.3),
199                              containerWidth - currentWidth);
200  maxPathWidth = Math.min(pathWidth, maxPathWidth);
201
202  var parentCrumb = lastSeparator.previousSibling;
203  var collapsedWidth = 0;
204  if (parentCrumb && pathWidth - maxPathWidth > parentCrumb.clientWidth) {
205    // At least one crumb is hidden completely (or almost completely).
206    // Show sign of hidden crumbs like this:
207    // root > some di... > ... > current directory.
208    parentCrumb.setAttribute('collapsed', '');
209    collapsedWidth = Math.min(maxPathWidth, parentCrumb.clientWidth);
210    maxPathWidth -= collapsedWidth;
211    if (parentCrumb.clientWidth != collapsedWidth)
212      parentCrumb.style.width = collapsedWidth + 'px';
213
214    lastSeparator = parentCrumb.previousSibling;
215    if (!lastSeparator)
216      return;
217    collapsedWidth += lastSeparator.clientWidth;
218    maxPathWidth = Math.max(0, maxPathWidth - lastSeparator.clientWidth);
219  }
220
221  pathWidth = 0;
222  for (var item = this.bc_.firstChild; item != lastSeparator;
223       item = item.nextSibling) {
224    // TODO(serya): Mixing access item.clientWidth and modifying style and
225    // attributes could cause multiple layout reflows.
226    if (pathWidth + item.clientWidth <= maxPathWidth) {
227      pathWidth += item.clientWidth;
228    } else if (pathWidth == maxPathWidth) {
229      item.style.width = '0';
230    } else if (item.classList.contains('separator')) {
231      // Do not truncate separator. Instead let the last crumb be longer.
232      item.style.width = '0';
233      maxPathWidth = pathWidth;
234    } else {
235      // Truncate the last visible crumb.
236      item.style.width = (maxPathWidth - pathWidth) + 'px';
237      pathWidth = maxPathWidth;
238    }
239  }
240
241  currentWidth = Math.min(currentWidth,
242                          containerWidth - pathWidth - collapsedWidth);
243  this.bc_.lastChild.style.width =
244      (currentWidth - lastCrumbSeparatorWidth) + 'px';
245};
246
247/**
248 * Hide breadcrumbs div.
249 */
250BreadcrumbsController.prototype.hide = function() {
251  this.bc_.hidden = true;
252};
253
254/**
255 * Handle a click event on a breadcrumb element.
256 * @param {Event} event The click event.
257 * @private
258 */
259BreadcrumbsController.prototype.onClick_ = function(event) {
260  if (!event.target.classList.contains('breadcrumb-path') ||
261      event.target.classList.contains('breadcrumb-last'))
262    return;
263
264  var newEvent = new Event('pathclick');
265  newEvent.entry = event.target.entry;
266  this.dispatchEvent(newEvent);
267};
268