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