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 |