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