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