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