| OLD | NEW |
| (Empty) |
| 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 * @constructor | |
| 12 */ | |
| 13 function BreadcrumbsController(div, metadataCache) { | |
| 14 this.metadataCache_ = metadataCache; | |
| 15 this.bc_ = div; | |
| 16 this.hideLast_ = false; | |
| 17 this.rootPath_ = null; | |
| 18 this.path_ = null; | |
| 19 this.rootPathOverride_ = null; | |
| 20 div.addEventListener('click', this.onClick_.bind(this)); | |
| 21 } | |
| 22 | |
| 23 /** | |
| 24 * Extends cr.EventTarget. | |
| 25 */ | |
| 26 BreadcrumbsController.prototype.__proto__ = cr.EventTarget.prototype; | |
| 27 | |
| 28 /** | |
| 29 * Whether to hide the last part of the path. | |
| 30 * @param {boolean} value True if hide. | |
| 31 */ | |
| 32 BreadcrumbsController.prototype.setHideLast = function(value) { | |
| 33 this.hideLast_ = value; | |
| 34 }; | |
| 35 | |
| 36 /** | |
| 37 * Update the breadcrumb display. | |
| 38 * TODO(haruki): Remove |rootPath|. It only needs |path|, as |rootPath| can be | |
| 39 * derived. | |
| 40 * | |
| 41 * @param {string} rootPath Path to root. | |
| 42 * @param {string} path Path to directory. | |
| 43 */ | |
| 44 BreadcrumbsController.prototype.update = function(rootPath, path) { | |
| 45 if (path == this.path_) | |
| 46 return; | |
| 47 | |
| 48 // Clear the content and store |path|. | |
| 49 this.bc_.textContent = ''; | |
| 50 this.rootPath_ = rootPath; | |
| 51 this.path_ = path; | |
| 52 | |
| 53 // determineRootPathOverride_() may call MetadataCache to retrieve | |
| 54 // shared-with-me property and call the callback asynchronously. | |
| 55 this.determineRootPathOverride_( | |
| 56 rootPath, path, function(requestedPath, rootPathOverride) { | |
| 57 if (requestedPath != path) { | |
| 58 // Another update() is called. Ignore this old callback. | |
| 59 return; | |
| 60 } | |
| 61 this.rootPathOverride_ = rootPathOverride; | |
| 62 this.updateInternal_(rootPath, path, rootPathOverride); | |
| 63 }.bind(this)); | |
| 64 }; | |
| 65 | |
| 66 /** | |
| 67 * Check the given path and determines if the breadcrumb should show "Shared | |
| 68 * with me" as the first crumb. RootDirectory.DRIVE_SHARED_WITH_ME is used as | |
| 69 * the first crumb if |path| is a shared-with-me directory outside "My Drive". | |
| 70 * See updateInternal_(). | |
| 71 * | |
| 72 * @param {string} rootPath Path to root. | |
| 73 * @param {string} path Path to directory. | |
| 74 * @param {function(string, !string)} callback Function to call with |path| and | |
| 75 * the determined root path override. | |
| 76 * @private | |
| 77 */ | |
| 78 BreadcrumbsController.prototype.determineRootPathOverride_ = | |
| 79 function(rootPath, path, callback) { | |
| 80 if (rootPath != RootDirectory.DRIVE + '/' + DriveSubRootDirectory.OTHER || | |
| 81 rootPath == path) { | |
| 82 callback(path, null); // No need for Drive properties. | |
| 83 return; | |
| 84 } | |
| 85 | |
| 86 var delimiterPos = path.indexOf('/', rootPath.length + 1); | |
| 87 var firstDirPath = delimiterPos > 0 ? path.substring(0, delimiterPos) : path; | |
| 88 var entryUrl = util.makeFilesystemUrl(firstDirPath); | |
| 89 | |
| 90 this.metadataCache_.getOne(entryUrl, 'drive', function(result) { | |
| 91 callback(path, | |
| 92 result && result.sharedWithMe ? | |
| 93 RootDirectory.DRIVE_SHARED_WITH_ME : | |
| 94 null); | |
| 95 }); | |
| 96 }; | |
| 97 | |
| 98 /** | |
| 99 * Update the breadcrumb display. | |
| 100 * @private | |
| 101 */ | |
| 102 BreadcrumbsController.prototype.updateInternal_ = function() { | |
| 103 var relativePath = | |
| 104 this.path_.substring(this.rootPath_.length).replace(/\/$/, ''); | |
| 105 var pathNames = relativePath.split('/'); | |
| 106 if (pathNames[0] == '') | |
| 107 pathNames.splice(0, 1); | |
| 108 | |
| 109 // We need a first breadcrumb for root, so placing last name from | |
| 110 // rootPath as first name of relativePath. | |
| 111 var rootPathNames = this.rootPath_.replace(/\/$/, '').split('/'); | |
| 112 pathNames.splice(0, 0, rootPathNames[rootPathNames.length - 1]); | |
| 113 rootPathNames.splice(rootPathNames.length - 1, 1); | |
| 114 var path = rootPathNames.join('/') + '/'; | |
| 115 | |
| 116 var doc = this.bc_.ownerDocument; | |
| 117 | |
| 118 var divAdded = false; | |
| 119 for (var i = 0; | |
| 120 i < (this.hideLast_ ? pathNames.length - 1 : pathNames.length); | |
| 121 i++) { | |
| 122 if (divAdded) { | |
| 123 var spacer = doc.createElement('div'); | |
| 124 spacer.className = 'separator'; | |
| 125 this.bc_.appendChild(spacer); | |
| 126 } | |
| 127 var pathName = pathNames[i]; | |
| 128 path += pathName; | |
| 129 | |
| 130 // We have a special case for the root crumb /drive/other, which contains | |
| 131 // the Drive entries not in "My Drive". It is hidden as this path is not | |
| 132 // meaningful for the user. | |
| 133 if (i == 0 && | |
| 134 path === RootDirectory.DRIVE + '/' + DriveSubRootDirectory.OTHER && | |
| 135 this.rootPathOverride_ == null) { | |
| 136 continue; | |
| 137 } | |
| 138 | |
| 139 // Create a crumb. | |
| 140 var div = doc.createElement('div'); | |
| 141 div.className = 'breadcrumb-path'; | |
| 142 div.textContent = i == 0 ? | |
| 143 PathUtil.getRootLabel(this.rootPathOverride_ || path) : | |
| 144 pathName; | |
| 145 | |
| 146 path = path + '/'; | |
| 147 | |
| 148 this.bc_.appendChild(div); | |
| 149 divAdded = true; | |
| 150 | |
| 151 if (i == pathNames.length - 1) | |
| 152 div.classList.add('breadcrumb-last'); | |
| 153 } | |
| 154 this.truncate(); | |
| 155 }; | |
| 156 | |
| 157 /** | |
| 158 * Updates breadcrumbs widths in order to truncate it properly. | |
| 159 */ | |
| 160 BreadcrumbsController.prototype.truncate = function() { | |
| 161 if (!this.bc_.firstChild) | |
| 162 return; | |
| 163 | |
| 164 // Assume style.width == clientWidth (items have no margins or paddings). | |
| 165 | |
| 166 for (var item = this.bc_.firstChild; item; item = item.nextSibling) { | |
| 167 item.removeAttribute('style'); | |
| 168 item.removeAttribute('collapsed'); | |
| 169 } | |
| 170 | |
| 171 var containerWidth = this.bc_.clientWidth; | |
| 172 | |
| 173 var pathWidth = 0; | |
| 174 var currentWidth = 0; | |
| 175 var lastSeparator; | |
| 176 for (var item = this.bc_.firstChild; item; item = item.nextSibling) { | |
| 177 if (item.className == 'separator') { | |
| 178 pathWidth += currentWidth; | |
| 179 currentWidth = item.clientWidth; | |
| 180 lastSeparator = item; | |
| 181 } else { | |
| 182 currentWidth += item.clientWidth; | |
| 183 } | |
| 184 } | |
| 185 if (pathWidth + currentWidth <= containerWidth) | |
| 186 return; | |
| 187 if (!lastSeparator) { | |
| 188 this.bc_.lastChild.style.width = Math.min(currentWidth, containerWidth) + | |
| 189 'px'; | |
| 190 return; | |
| 191 } | |
| 192 var lastCrumbSeparatorWidth = lastSeparator.clientWidth; | |
| 193 // Current directory name may occupy up to 70% of space or even more if the | |
| 194 // path is short. | |
| 195 var maxPathWidth = Math.max(Math.round(containerWidth * 0.3), | |
| 196 containerWidth - currentWidth); | |
| 197 maxPathWidth = Math.min(pathWidth, maxPathWidth); | |
| 198 | |
| 199 var parentCrumb = lastSeparator.previousSibling; | |
| 200 var collapsedWidth = 0; | |
| 201 if (parentCrumb && pathWidth - maxPathWidth > parentCrumb.clientWidth) { | |
| 202 // At least one crumb is hidden completely (or almost completely). | |
| 203 // Show sign of hidden crumbs like this: | |
| 204 // root > some di... > ... > current directory. | |
| 205 parentCrumb.setAttribute('collapsed', ''); | |
| 206 collapsedWidth = Math.min(maxPathWidth, parentCrumb.clientWidth); | |
| 207 maxPathWidth -= collapsedWidth; | |
| 208 if (parentCrumb.clientWidth != collapsedWidth) | |
| 209 parentCrumb.style.width = collapsedWidth + 'px'; | |
| 210 | |
| 211 lastSeparator = parentCrumb.previousSibling; | |
| 212 if (!lastSeparator) | |
| 213 return; | |
| 214 collapsedWidth += lastSeparator.clientWidth; | |
| 215 maxPathWidth = Math.max(0, maxPathWidth - lastSeparator.clientWidth); | |
| 216 } | |
| 217 | |
| 218 pathWidth = 0; | |
| 219 for (var item = this.bc_.firstChild; item != lastSeparator; | |
| 220 item = item.nextSibling) { | |
| 221 // TODO(serya): Mixing access item.clientWidth and modifying style and | |
| 222 // attributes could cause multiple layout reflows. | |
| 223 if (pathWidth + item.clientWidth <= maxPathWidth) { | |
| 224 pathWidth += item.clientWidth; | |
| 225 } else if (pathWidth == maxPathWidth) { | |
| 226 item.style.width = '0'; | |
| 227 } else if (item.classList.contains('separator')) { | |
| 228 // Do not truncate separator. Instead let the last crumb be longer. | |
| 229 item.style.width = '0'; | |
| 230 maxPathWidth = pathWidth; | |
| 231 } else { | |
| 232 // Truncate the last visible crumb. | |
| 233 item.style.width = (maxPathWidth - pathWidth) + 'px'; | |
| 234 pathWidth = maxPathWidth; | |
| 235 } | |
| 236 } | |
| 237 | |
| 238 currentWidth = Math.min(currentWidth, | |
| 239 containerWidth - pathWidth - collapsedWidth); | |
| 240 this.bc_.lastChild.style.width = | |
| 241 (currentWidth - lastCrumbSeparatorWidth) + 'px'; | |
| 242 }; | |
| 243 | |
| 244 /** | |
| 245 * Show breadcrumbs. | |
| 246 * @param {string} rootPath Path to root. | |
| 247 * @param {string} path Path to directory. | |
| 248 */ | |
| 249 BreadcrumbsController.prototype.show = function(rootPath, path) { | |
| 250 this.bc_.hidden = false; | |
| 251 this.update(rootPath, path); | |
| 252 }; | |
| 253 | |
| 254 /** | |
| 255 * Hide breadcrumbs div. | |
| 256 */ | |
| 257 BreadcrumbsController.prototype.hide = function() { | |
| 258 this.bc_.hidden = true; | |
| 259 }; | |
| 260 | |
| 261 /** | |
| 262 * Handle a click event on a breadcrumb element. | |
| 263 * @param {Event} event The click event. | |
| 264 * @private | |
| 265 */ | |
| 266 BreadcrumbsController.prototype.onClick_ = function(event) { | |
| 267 var path = this.getTargetPath(event); | |
| 268 if (!path) | |
| 269 return; | |
| 270 | |
| 271 var newEvent = new Event('pathclick'); | |
| 272 newEvent.path = path; | |
| 273 this.dispatchEvent(newEvent); | |
| 274 }; | |
| 275 | |
| 276 /** | |
| 277 * Returns path associated with the event target. Returns empty string for | |
| 278 * inactive elements: separators, empty space and the last chunk. | |
| 279 * @param {Event} event The UI event. | |
| 280 * @return {string} Full path or empty string. | |
| 281 */ | |
| 282 BreadcrumbsController.prototype.getTargetPath = function(event) { | |
| 283 if (!event.target.classList.contains('breadcrumb-path') || | |
| 284 event.target.classList.contains('breadcrumb-last')) { | |
| 285 return ''; | |
| 286 } | |
| 287 | |
| 288 var items = this.bc_.querySelectorAll('.breadcrumb-path'); | |
| 289 var path = this.rootPath_; | |
| 290 | |
| 291 if (event.target == items[0]) { | |
| 292 // The first crumb can be overridden. | |
| 293 return this.rootPathOverride_ || path; | |
| 294 } else { | |
| 295 for (var i = 1; items[i - 1] != event.target; i++) { | |
| 296 path += '/' + items[i].textContent; | |
| 297 } | |
| 298 } | |
| 299 return path; | |
| 300 }; | |
| 301 | |
| 302 /** | |
| 303 * Returns the breadcrumbs container. | |
| 304 * @return {HTMLElement} Breadcumbs container HTML element. | |
| 305 */ | |
| 306 BreadcrumbsController.prototype.getContainer = function() { | |
| 307 return this.bc_; | |
| 308 }; | |
| OLD | NEW |