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