OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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 * Namespace for utility functions. | |
9 */ | |
10 var filelist = {}; | |
11 | |
12 /** | |
13 * Custom column model for advanced auto-resizing. | |
14 * | |
15 * @param {Array.<cr.ui.table.TableColumn>} tableColumns Table columns. | |
16 * @extends {cr.ui.table.TableColumnModel} | |
17 * @constructor | |
18 */ | |
19 function FileTableColumnModel(tableColumns) { | |
20 cr.ui.table.TableColumnModel.call(this, tableColumns); | |
21 } | |
22 | |
23 /** | |
24 * The columns whose index is less than the constant are resizable. | |
25 * @const | |
26 * @type {number} | |
27 * @private | |
28 */ | |
29 FileTableColumnModel.RESIZABLE_LENGTH_ = 4; | |
30 | |
31 /** | |
32 * Inherits from cr.ui.TableColumnModel. | |
33 */ | |
34 FileTableColumnModel.prototype.__proto__ = | |
35 cr.ui.table.TableColumnModel.prototype; | |
36 | |
37 /** | |
38 * Minimum width of column. | |
39 * @const | |
40 * @type {number} | |
41 * @private | |
42 */ | |
43 FileTableColumnModel.MIN_WIDTH_ = 10; | |
44 | |
45 /** | |
46 * Sets column width so that the column dividers move to the specified position. | |
47 * This function also check the width of each column and keep the width larger | |
48 * than MIN_WIDTH_. | |
49 * | |
50 * @private | |
51 * @param {Array.<number>} newPos Positions of each column dividers. | |
52 */ | |
53 FileTableColumnModel.prototype.applyColumnPositions_ = function(newPos) { | |
54 // Check the minimum width and adjust the positions. | |
55 for (var i = 0; i < newPos.length - 2; i++) { | |
56 if (newPos[i + 1] - newPos[i] < FileTableColumnModel.MIN_WIDTH_) { | |
57 newPos[i + 1] = newPos[i] + FileTableColumnModel.MIN_WIDTH_; | |
58 } | |
59 } | |
60 for (var i = newPos.length - 1; i >= 2; i--) { | |
61 if (newPos[i] - newPos[i - 1] < FileTableColumnModel.MIN_WIDTH_) { | |
62 newPos[i - 1] = newPos[i] - FileTableColumnModel.MIN_WIDTH_; | |
63 } | |
64 } | |
65 // Set the new width of columns | |
66 for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) { | |
67 this.columns_[i].width = newPos[i + 1] - newPos[i]; | |
68 } | |
69 }; | |
70 | |
71 /** | |
72 * Normalizes widths to make their sum 100% if possible. Uses the proportional | |
73 * approach with some additional constraints. | |
74 * | |
75 * @param {number} contentWidth Target width. | |
76 * @override | |
77 */ | |
78 FileTableColumnModel.prototype.normalizeWidths = function(contentWidth) { | |
79 var totalWidth = 0; | |
80 var fixedWidth = 0; | |
81 // Some columns have fixed width. | |
82 for (var i = 0; i < this.columns_.length; i++) { | |
83 if (i < FileTableColumnModel.RESIZABLE_LENGTH_) | |
84 totalWidth += this.columns_[i].width; | |
85 else | |
86 fixedWidth += this.columns_[i].width; | |
87 } | |
88 var newTotalWidth = Math.max(contentWidth - fixedWidth, 0); | |
89 var positions = [0]; | |
90 var sum = 0; | |
91 for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) { | |
92 var column = this.columns_[i]; | |
93 sum += column.width; | |
94 // Faster alternative to Math.floor for non-negative numbers. | |
95 positions[i + 1] = ~~(newTotalWidth * sum / totalWidth); | |
96 } | |
97 this.applyColumnPositions_(positions); | |
98 }; | |
99 | |
100 /** | |
101 * Handles to the start of column resizing by splitters. | |
102 */ | |
103 FileTableColumnModel.prototype.handleSplitterDragStart = function() { | |
104 this.columnPos_ = [0]; | |
105 for (var i = 0; i < this.columns_.length; i++) { | |
106 this.columnPos_[i + 1] = this.columns_[i].width + this.columnPos_[i]; | |
107 } | |
108 }; | |
109 | |
110 /** | |
111 * Handles to the end of column resizing by splitters. | |
112 */ | |
113 FileTableColumnModel.prototype.handleSplitterDragEnd = function() { | |
114 this.columnPos_ = null; | |
115 }; | |
116 | |
117 /** | |
118 * Sets the width of column with keeping the total width of table. | |
119 * @param {number} columnIndex Index of column that is resized. | |
120 * @param {number} columnWidth New width of the column. | |
121 */ | |
122 FileTableColumnModel.prototype.setWidthAndKeepTotal = function( | |
123 columnIndex, columnWidth) { | |
124 // Skip to resize 'selection' column | |
125 if (columnIndex < 0 || | |
126 columnIndex >= FileTableColumnModel.RESIZABLE_LENGTH_ || | |
127 !this.columnPos_) { | |
128 return; | |
129 } | |
130 | |
131 // Calculate new positions of column splitters. | |
132 var newPosStart = | |
133 this.columnPos_[columnIndex] + Math.max(columnWidth, | |
134 FileTableColumnModel.MIN_WIDTH_); | |
135 var newPos = []; | |
136 var posEnd = this.columnPos_[FileTableColumnModel.RESIZABLE_LENGTH_]; | |
137 for (var i = 0; i < columnIndex + 1; i++) { | |
138 newPos[i] = this.columnPos_[i]; | |
139 } | |
140 for (var i = columnIndex + 1; | |
141 i < FileTableColumnModel.RESIZABLE_LENGTH_; | |
142 i++) { | |
143 var posStart = this.columnPos_[columnIndex + 1]; | |
144 newPos[i] = (posEnd - newPosStart) * | |
145 (this.columnPos_[i] - posStart) / | |
146 (posEnd - posStart) + | |
147 newPosStart; | |
148 // Faster alternative to Math.floor for non-negative numbers. | |
149 newPos[i] = ~~newPos[i]; | |
150 } | |
151 newPos[columnIndex] = this.columnPos_[columnIndex]; | |
152 newPos[FileTableColumnModel.RESIZABLE_LENGTH_] = posEnd; | |
153 this.applyColumnPositions_(newPos); | |
154 | |
155 // Notifiy about resizing | |
156 cr.dispatchSimpleEvent(this, 'resize'); | |
157 }; | |
158 | |
159 /** | |
160 * Custom splitter that resizes column with retaining the sum of all the column | |
161 * width. | |
162 */ | |
163 var FileTableSplitter = cr.ui.define('div'); | |
164 | |
165 /** | |
166 * Inherits from cr.ui.TableSplitter. | |
167 */ | |
168 FileTableSplitter.prototype.__proto__ = cr.ui.TableSplitter.prototype; | |
169 | |
170 /** | |
171 * Handles the drag start event. | |
172 */ | |
173 FileTableSplitter.prototype.handleSplitterDragStart = function() { | |
174 cr.ui.TableSplitter.prototype.handleSplitterDragStart.call(this); | |
175 this.table_.columnModel.handleSplitterDragStart(); | |
176 }; | |
177 | |
178 /** | |
179 * Handles the drag move event. | |
180 * @param {number} deltaX Horizontal mouse move offset. | |
181 */ | |
182 FileTableSplitter.prototype.handleSplitterDragMove = function(deltaX) { | |
183 this.table_.columnModel.setWidthAndKeepTotal(this.columnIndex, | |
184 this.columnWidth_ + deltaX, | |
185 true); | |
186 }; | |
187 | |
188 /** | |
189 * Handles the drag end event. | |
190 */ | |
191 FileTableSplitter.prototype.handleSplitterDragEnd = function() { | |
192 cr.ui.TableSplitter.prototype.handleSplitterDragEnd.call(this); | |
193 this.table_.columnModel.handleSplitterDragEnd(); | |
194 }; | |
195 | |
196 /** | |
197 * File list Table View. | |
198 * @constructor | |
199 */ | |
200 function FileTable() { | |
201 throw new Error('Designed to decorate elements'); | |
202 } | |
203 | |
204 /** | |
205 * Inherits from cr.ui.Table. | |
206 */ | |
207 FileTable.prototype.__proto__ = cr.ui.Table.prototype; | |
208 | |
209 /** | |
210 * Decorates the element. | |
211 * @param {HTMLElement} self Table to decorate. | |
212 * @param {MetadataCache} metadataCache To retrieve metadata. | |
213 * @param {boolean} fullPage True if it's full page File Manager, | |
214 * False if a file open/save dialog. | |
215 */ | |
216 FileTable.decorate = function(self, metadataCache, fullPage) { | |
217 cr.ui.Table.decorate(self); | |
218 self.__proto__ = FileTable.prototype; | |
219 self.metadataCache_ = metadataCache; | |
220 self.collator_ = Intl.Collator([], {numeric: true, sensitivity: 'base'}); | |
221 | |
222 var columns = [ | |
223 new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'), | |
224 fullPage ? 386 : 324), | |
225 new cr.ui.table.TableColumn('size', str('SIZE_COLUMN_LABEL'), | |
226 110, true), | |
227 new cr.ui.table.TableColumn('type', str('TYPE_COLUMN_LABEL'), | |
228 fullPage ? 110 : 110), | |
229 new cr.ui.table.TableColumn('modificationTime', | |
230 str('DATE_COLUMN_LABEL'), | |
231 fullPage ? 150 : 210) | |
232 ]; | |
233 | |
234 columns[0].renderFunction = self.renderName_.bind(self); | |
235 columns[1].renderFunction = self.renderSize_.bind(self); | |
236 columns[1].defaultOrder = 'desc'; | |
237 columns[2].renderFunction = self.renderType_.bind(self); | |
238 columns[3].renderFunction = self.renderDate_.bind(self); | |
239 columns[3].defaultOrder = 'desc'; | |
240 | |
241 var tableColumnModelClass; | |
242 tableColumnModelClass = FileTableColumnModel; | |
243 if (self.showCheckboxes) { | |
244 columns.push(new cr.ui.table.TableColumn('selection', | |
245 '', | |
246 50, true)); | |
247 columns[4].renderFunction = self.renderSelection_.bind(self); | |
248 columns[4].headerRenderFunction = | |
249 self.renderSelectionColumnHeader_.bind(self); | |
250 columns[4].fixed = true; | |
251 } | |
252 | |
253 var columnModel = Object.create(tableColumnModelClass.prototype, { | |
254 /** | |
255 * The number of columns. | |
256 * @type {number} | |
257 */ | |
258 size: { | |
259 /** | |
260 * @this {FileTableColumnModel} | |
261 * @return {number} Number of columns. | |
262 */ | |
263 get: function() { | |
264 return this.totalSize; | |
265 } | |
266 }, | |
267 | |
268 /** | |
269 * The number of columns. | |
270 * @type {number} | |
271 */ | |
272 totalSize: { | |
273 /** | |
274 * @this {FileTableColumnModel} | |
275 * @return {number} Number of columns. | |
276 */ | |
277 get: function() { | |
278 return columns.length; | |
279 } | |
280 }, | |
281 | |
282 /** | |
283 * Obtains a column by the specified horizontal position. | |
284 */ | |
285 getHitColumn: { | |
286 /** | |
287 * @this {FileTableColumnModel} | |
288 * @param {number} x Horizontal position. | |
289 * @return {object} The object that contains column index, column width, | |
290 * and hitPosition where the horizontal position is hit in the column. | |
291 */ | |
292 value: function(x) { | |
293 for (var i = 0; x >= this.columns_[i].width; i++) { | |
294 x -= this.columns_[i].width; | |
295 } | |
296 if (i >= this.columns_.length) | |
297 return null; | |
298 return {index: i, hitPosition: x, width: this.columns_[i].width}; | |
299 } | |
300 } | |
301 }); | |
302 | |
303 tableColumnModelClass.call(columnModel, columns); | |
304 self.columnModel = columnModel; | |
305 self.setDateTimeFormat(true); | |
306 self.setRenderFunction(self.renderTableRow_.bind(self, | |
307 self.getRenderFunction())); | |
308 | |
309 self.scrollBar_ = MainPanelScrollBar(); | |
310 self.scrollBar_.initialize(self, self.list); | |
311 // Keep focus on the file list when clicking on the header. | |
312 self.header.addEventListener('mousedown', function(e) { | |
313 self.list.focus(); | |
314 e.preventDefault(); | |
315 }); | |
316 | |
317 var handleSelectionChange = function() { | |
318 var selectAll = self.querySelector('#select-all-checkbox'); | |
319 if (selectAll) | |
320 self.updateSelectAllCheckboxState_(selectAll); | |
321 }; | |
322 | |
323 self.relayoutAggregation_ = | |
324 new AsyncUtil.Aggregation(self.relayoutImmediately_.bind(self)); | |
325 | |
326 Object.defineProperty(self.list_, 'selectionModel', { | |
327 /** | |
328 * @this {cr.ui.List} | |
329 * @return {cr.ui.ListSelectionModel} The current selection model. | |
330 */ | |
331 get: function() { | |
332 return this.selectionModel_; | |
333 }, | |
334 /** | |
335 * @this {cr.ui.List} | |
336 */ | |
337 set: function(value) { | |
338 var sm = this.selectionModel; | |
339 if (sm) | |
340 sm.removeEventListener('change', handleSelectionChange); | |
341 | |
342 util.callInheritedSetter(this, 'selectionModel', value); | |
343 sm = value; | |
344 | |
345 if (sm) | |
346 sm.addEventListener('change', handleSelectionChange); | |
347 handleSelectionChange(); | |
348 } | |
349 }); | |
350 | |
351 // Override header#redraw to use FileTableSplitter. | |
352 self.header_.redraw = function() { | |
353 this.__proto__.redraw.call(this); | |
354 // Extend table splitters | |
355 var splitters = this.querySelectorAll('.table-header-splitter'); | |
356 for (var i = 0; i < splitters.length; i++) { | |
357 if (splitters[i] instanceof FileTableSplitter) | |
358 continue; | |
359 FileTableSplitter.decorate(splitters[i]); | |
360 } | |
361 }; | |
362 | |
363 // Save the last selection. This is used by shouldStartDragSelection. | |
364 self.list.addEventListener('mousedown', function(e) { | |
365 this.lastSelection_ = this.selectionModel.selectedIndexes; | |
366 }.bind(self), true); | |
367 self.list.shouldStartDragSelection = | |
368 self.shouldStartDragSelection_.bind(self); | |
369 | |
370 /** | |
371 * Obtains the index list of elements that are hit by the point or the | |
372 * rectangle. | |
373 * | |
374 * @param {number} x X coordinate value. | |
375 * @param {number} y Y coordinate value. | |
376 * @param {=number} opt_width Width of the coordinate. | |
377 * @param {=number} opt_height Height of the coordinate. | |
378 * @return {Array.<number>} Index list of hit elements. | |
379 */ | |
380 self.list.getHitElements = function(x, y, opt_width, opt_height) { | |
381 var currentSelection = []; | |
382 var bottom = y + (opt_height || 0); | |
383 for (var i = 0; i < this.selectionModel_.length; i++) { | |
384 var itemMetrics = this.getHeightsForIndex_(i); | |
385 if (itemMetrics.top < bottom && itemMetrics.top + itemMetrics.height >= y) | |
386 currentSelection.push(i); | |
387 } | |
388 return currentSelection; | |
389 }; | |
390 }; | |
391 | |
392 /** | |
393 * Sets date and time format. | |
394 * @param {boolean} use12hourClock True if 12 hours clock, False if 24 hours. | |
395 */ | |
396 FileTable.prototype.setDateTimeFormat = function(use12hourClock) { | |
397 this.timeFormatter_ = Intl.DateTimeFormat( | |
398 [] /* default locale */, | |
399 {hour: 'numeric', minute: 'numeric', | |
400 hour12: use12hourClock}); | |
401 this.dateFormatter_ = Intl.DateTimeFormat( | |
402 [] /* default locale */, | |
403 {year: 'numeric', month: 'short', day: 'numeric', | |
404 hour: 'numeric', minute: 'numeric', | |
405 hour12: use12hourClock}); | |
406 }; | |
407 | |
408 /** | |
409 * Obtains if the drag selection should be start or not by referring the mouse | |
410 * event. | |
411 * @param {MouseEvent} event Drag start event. | |
412 * @return {boolean} True if the mouse is hit to the background of the list. | |
413 * @private | |
414 */ | |
415 FileTable.prototype.shouldStartDragSelection_ = function(event) { | |
416 // If the shift key is pressed, it should starts drag selection. | |
417 if (event.shiftKey) | |
418 return true; | |
419 | |
420 // If the position values are negative, it points the out of list. | |
421 // It should start the drag selection. | |
422 var pos = DragSelector.getScrolledPosition(this.list, event); | |
423 if (!pos) | |
424 return false; | |
425 if (pos.x < 0 || pos.y < 0) | |
426 return true; | |
427 | |
428 // If the item index is out of range, it should start the drag selection. | |
429 var itemHeight = this.list.measureItem().height; | |
430 // Faster alternative to Math.floor for non-negative numbers. | |
431 var itemIndex = ~~(pos.y / itemHeight); | |
432 if (itemIndex >= this.list.dataModel.length) | |
433 return true; | |
434 | |
435 // If the pointed item is already selected, it should not start the drag | |
436 // selection. | |
437 if (this.lastSelection_.indexOf(itemIndex) != -1) | |
438 return false; | |
439 | |
440 // If the horizontal value is not hit to column, it should start the drag | |
441 // selection. | |
442 var hitColumn = this.columnModel.getHitColumn(pos.x); | |
443 if (!hitColumn) | |
444 return true; | |
445 | |
446 // Check if the point is on the column contents or not. | |
447 var item = this.list.getListItemByIndex(itemIndex); | |
448 switch (this.columnModel.columns_[hitColumn.index].id) { | |
449 case 'name': | |
450 var spanElement = item.querySelector('.filename-label span'); | |
451 var spanRect = spanElement.getBoundingClientRect(); | |
452 // The this.list.cachedBounds_ object is set by | |
453 // DragSelector.getScrolledPosition. | |
454 if (!this.list.cachedBounds) | |
455 return true; | |
456 var textRight = | |
457 spanRect.left - this.list.cachedBounds.left + spanRect.width; | |
458 return textRight <= hitColumn.hitPosition; | |
459 default: | |
460 return true; | |
461 } | |
462 }; | |
463 | |
464 /** | |
465 * Update check and disable states of the 'Select all' checkbox. | |
466 * @param {HTMLInputElement} checkbox The checkbox. If not passed, using | |
467 * the default one. | |
468 * @private | |
469 */ | |
470 FileTable.prototype.updateSelectAllCheckboxState_ = function(checkbox) { | |
471 // TODO(serya): introduce this.selectionModel.selectedCount. | |
472 checkbox.checked = this.dataModel.length > 0 && | |
473 this.dataModel.length == this.selectionModel.selectedIndexes.length; | |
474 checkbox.disabled = this.dataModel.length == 0; | |
475 }; | |
476 | |
477 /** | |
478 * Prepares the data model to be sorted by columns. | |
479 * @param {cr.ui.ArrayDataModel} dataModel Data model to prepare. | |
480 */ | |
481 FileTable.prototype.setupCompareFunctions = function(dataModel) { | |
482 dataModel.setCompareFunction('name', | |
483 this.compareName_.bind(this)); | |
484 dataModel.setCompareFunction('modificationTime', | |
485 this.compareMtime_.bind(this)); | |
486 dataModel.setCompareFunction('size', | |
487 this.compareSize_.bind(this)); | |
488 dataModel.setCompareFunction('type', | |
489 this.compareType_.bind(this)); | |
490 }; | |
491 | |
492 /** | |
493 * Render the Name column of the detail table. | |
494 * | |
495 * Invoked by cr.ui.Table when a file needs to be rendered. | |
496 * | |
497 * @param {Entry} entry The Entry object to render. | |
498 * @param {string} columnId The id of the column to be rendered. | |
499 * @param {cr.ui.Table} table The table doing the rendering. | |
500 * @return {HTMLDivElement} Created element. | |
501 * @private | |
502 */ | |
503 FileTable.prototype.renderName_ = function(entry, columnId, table) { | |
504 var label = this.ownerDocument.createElement('div'); | |
505 label.appendChild(this.renderIconType_(entry, columnId, table)); | |
506 label.entry = entry; | |
507 label.className = 'detail-name'; | |
508 label.appendChild(filelist.renderFileNameLabel(this.ownerDocument, entry)); | |
509 return label; | |
510 }; | |
511 | |
512 /** | |
513 * Render the Selection column of the detail table. | |
514 * | |
515 * Invoked by cr.ui.Table when a file needs to be rendered. | |
516 * | |
517 * @param {Entry} entry The Entry object to render. | |
518 * @param {string} columnId The id of the column to be rendered. | |
519 * @param {cr.ui.Table} table The table doing the rendering. | |
520 * @return {HTMLDivElement} Created element. | |
521 * @private | |
522 */ | |
523 FileTable.prototype.renderSelection_ = function(entry, columnId, table) { | |
524 var label = this.ownerDocument.createElement('div'); | |
525 label.className = 'selection-label'; | |
526 if (this.selectionModel.multiple) { | |
527 var checkBox = this.ownerDocument.createElement('input'); | |
528 filelist.decorateSelectionCheckbox(checkBox, entry, this.list); | |
529 label.appendChild(checkBox); | |
530 } | |
531 return label; | |
532 }; | |
533 | |
534 /** | |
535 * Render the Size column of the detail table. | |
536 * | |
537 * @param {Entry} entry The Entry object to render. | |
538 * @param {string} columnId The id of the column to be rendered. | |
539 * @param {cr.ui.Table} table The table doing the rendering. | |
540 * @return {HTMLDivElement} Created element. | |
541 * @private | |
542 */ | |
543 FileTable.prototype.renderSize_ = function(entry, columnId, table) { | |
544 var div = this.ownerDocument.createElement('div'); | |
545 div.className = 'size'; | |
546 this.updateSize_( | |
547 div, entry, this.metadataCache_.getCached(entry, 'filesystem')); | |
548 | |
549 return div; | |
550 }; | |
551 | |
552 /** | |
553 * Sets up or updates the size cell. | |
554 * | |
555 * @param {HTMLDivElement} div The table cell. | |
556 * @param {Entry} entry The corresponding entry. | |
557 * @param {Object} filesystemProps Metadata. | |
558 * @private | |
559 */ | |
560 FileTable.prototype.updateSize_ = function(div, entry, filesystemProps) { | |
561 if (!filesystemProps) { | |
562 div.textContent = '...'; | |
563 } else if (filesystemProps.size == -1) { | |
564 div.textContent = '--'; | |
565 } else if (filesystemProps.size == 0 && | |
566 FileType.isHosted(entry)) { | |
567 div.textContent = '--'; | |
568 } else { | |
569 div.textContent = util.bytesToString(filesystemProps.size); | |
570 } | |
571 }; | |
572 | |
573 /** | |
574 * Render the Type column of the detail table. | |
575 * | |
576 * @param {Entry} entry The Entry object to render. | |
577 * @param {string} columnId The id of the column to be rendered. | |
578 * @param {cr.ui.Table} table The table doing the rendering. | |
579 * @return {HTMLDivElement} Created element. | |
580 * @private | |
581 */ | |
582 FileTable.prototype.renderType_ = function(entry, columnId, table) { | |
583 var div = this.ownerDocument.createElement('div'); | |
584 div.className = 'type'; | |
585 div.textContent = FileType.getTypeString(entry); | |
586 return div; | |
587 }; | |
588 | |
589 /** | |
590 * Render the Date column of the detail table. | |
591 * | |
592 * @param {Entry} entry The Entry object to render. | |
593 * @param {string} columnId The id of the column to be rendered. | |
594 * @param {cr.ui.Table} table The table doing the rendering. | |
595 * @return {HTMLDivElement} Created element. | |
596 * @private | |
597 */ | |
598 FileTable.prototype.renderDate_ = function(entry, columnId, table) { | |
599 var div = this.ownerDocument.createElement('div'); | |
600 div.className = 'date'; | |
601 | |
602 this.updateDate_(div, | |
603 this.metadataCache_.getCached(entry, 'filesystem')); | |
604 return div; | |
605 }; | |
606 | |
607 /** | |
608 * Sets up or updates the date cell. | |
609 * | |
610 * @param {HTMLDivElement} div The table cell. | |
611 * @param {Object} filesystemProps Metadata. | |
612 * @private | |
613 */ | |
614 FileTable.prototype.updateDate_ = function(div, filesystemProps) { | |
615 if (!filesystemProps) { | |
616 div.textContent = '...'; | |
617 return; | |
618 } | |
619 | |
620 var modTime = filesystemProps.modificationTime; | |
621 var today = new Date(); | |
622 today.setHours(0); | |
623 today.setMinutes(0); | |
624 today.setSeconds(0); | |
625 today.setMilliseconds(0); | |
626 | |
627 /** | |
628 * Number of milliseconds in a day. | |
629 */ | |
630 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; | |
631 | |
632 if (modTime >= today && | |
633 modTime < today.getTime() + MILLISECONDS_IN_DAY) { | |
634 div.textContent = strf('TIME_TODAY', this.timeFormatter_.format(modTime)); | |
635 } else if (modTime >= today - MILLISECONDS_IN_DAY && modTime < today) { | |
636 div.textContent = strf('TIME_YESTERDAY', | |
637 this.timeFormatter_.format(modTime)); | |
638 } else { | |
639 div.textContent = | |
640 this.dateFormatter_.format(filesystemProps.modificationTime); | |
641 } | |
642 }; | |
643 | |
644 /** | |
645 * Updates the file metadata in the table item. | |
646 * | |
647 * @param {Element} item Table item. | |
648 * @param {Entry} entry File entry. | |
649 */ | |
650 FileTable.prototype.updateFileMetadata = function(item, entry) { | |
651 var props = this.metadataCache_.getCached(entry, 'filesystem'); | |
652 this.updateDate_(item.querySelector('.date'), props); | |
653 this.updateSize_(item.querySelector('.size'), entry, props); | |
654 }; | |
655 | |
656 /** | |
657 * Updates list items 'in place' on metadata change. | |
658 * @param {string} type Type of metadata change. | |
659 * @param {Object.<sting, Object>} propsMap Map from entry URLs to metadata | |
660 * properties. | |
661 */ | |
662 FileTable.prototype.updateListItemsMetadata = function(type, propsMap) { | |
663 var forEachCell = function(selector, callback) { | |
664 var cells = this.querySelectorAll(selector); | |
665 for (var i = 0; i < cells.length; i++) { | |
666 var cell = cells[i]; | |
667 var listItem = this.list_.getListItemAncestor(cell); | |
668 var entry = this.dataModel.item(listItem.listIndex); | |
669 if (entry) { | |
670 var props = propsMap[entry.toURL()]; | |
671 if (props) | |
672 callback.call(this, cell, entry, props, listItem); | |
673 } | |
674 } | |
675 }.bind(this); | |
676 if (type == 'filesystem') { | |
677 forEachCell('.table-row-cell > .date', function(item, entry, props) { | |
678 this.updateDate_(item, props); | |
679 }); | |
680 forEachCell('.table-row-cell > .size', function(item, entry, props) { | |
681 this.updateSize_(item, entry, props); | |
682 }); | |
683 } else if (type == 'drive') { | |
684 // The cell name does not matter as the entire list item is needed. | |
685 forEachCell('.table-row-cell > .date', | |
686 function(item, entry, props, listItem) { | |
687 filelist.updateListItemDriveProps(listItem, props); | |
688 }); | |
689 } | |
690 }; | |
691 | |
692 /** | |
693 * Compare by mtime first, then by name. | |
694 * @param {Entry} a First entry. | |
695 * @param {Entry} b Second entry. | |
696 * @return {number} Compare result. | |
697 * @private | |
698 */ | |
699 FileTable.prototype.compareName_ = function(a, b) { | |
700 return this.collator_.compare(a.name, b.name); | |
701 }; | |
702 | |
703 /** | |
704 * Compare by mtime first, then by name. | |
705 * @param {Entry} a First entry. | |
706 * @param {Entry} b Second entry. | |
707 * @return {number} Compare result. | |
708 * @private | |
709 */ | |
710 FileTable.prototype.compareMtime_ = function(a, b) { | |
711 var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); | |
712 var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0; | |
713 | |
714 var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); | |
715 var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0; | |
716 | |
717 if (aTime > bTime) | |
718 return 1; | |
719 | |
720 if (aTime < bTime) | |
721 return -1; | |
722 | |
723 return this.collator_.compare(a.name, b.name); | |
724 }; | |
725 | |
726 /** | |
727 * Compare by size first, then by name. | |
728 * @param {Entry} a First entry. | |
729 * @param {Entry} b Second entry. | |
730 * @return {number} Compare result. | |
731 * @private | |
732 */ | |
733 FileTable.prototype.compareSize_ = function(a, b) { | |
734 var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); | |
735 var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0; | |
736 | |
737 var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); | |
738 var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0; | |
739 | |
740 if (aSize != bSize) return aSize - bSize; | |
741 return this.collator_.compare(a.name, b.name); | |
742 }; | |
743 | |
744 /** | |
745 * Compare by type first, then by subtype and then by name. | |
746 * @param {Entry} a First entry. | |
747 * @param {Entry} b Second entry. | |
748 * @return {number} Compare result. | |
749 * @private | |
750 */ | |
751 FileTable.prototype.compareType_ = function(a, b) { | |
752 // Directories precede files. | |
753 if (a.isDirectory != b.isDirectory) | |
754 return Number(b.isDirectory) - Number(a.isDirectory); | |
755 | |
756 var aType = FileType.getTypeString(a); | |
757 var bType = FileType.getTypeString(b); | |
758 | |
759 var result = this.collator_.compare(aType, bType); | |
760 if (result != 0) | |
761 return result; | |
762 | |
763 return this.collator_.compare(a.name, b.name); | |
764 }; | |
765 | |
766 /** | |
767 * Renders table row. | |
768 * @param {function(Entry, cr.ui.Table)} baseRenderFunction Base renderer. | |
769 * @param {Entry} entry Corresponding entry. | |
770 * @return {HTMLLiElement} Created element. | |
771 * @private | |
772 */ | |
773 FileTable.prototype.renderTableRow_ = function(baseRenderFunction, entry) { | |
774 var item = baseRenderFunction(entry, this); | |
775 filelist.decorateListItem(item, entry, this.metadataCache_); | |
776 return item; | |
777 }; | |
778 | |
779 /** | |
780 * Renders the name column header. | |
781 * @param {string} name Localized column name. | |
782 * @return {HTMLLiElement} Created element. | |
783 * @private | |
784 */ | |
785 FileTable.prototype.renderNameColumnHeader_ = function(name) { | |
786 if (!this.selectionModel.multiple) | |
787 return this.ownerDocument.createTextNode(name); | |
788 | |
789 var input = this.ownerDocument.createElement('input'); | |
790 input.setAttribute('type', 'checkbox'); | |
791 input.setAttribute('tabindex', -1); | |
792 input.id = 'select-all-checkbox'; | |
793 input.className = 'common'; | |
794 | |
795 this.updateSelectAllCheckboxState_(input); | |
796 | |
797 input.addEventListener('click', function(event) { | |
798 if (input.checked) | |
799 this.selectionModel.selectAll(); | |
800 else | |
801 this.selectionModel.unselectAll(); | |
802 event.stopPropagation(); | |
803 }.bind(this)); | |
804 | |
805 var fragment = this.ownerDocument.createDocumentFragment(); | |
806 fragment.appendChild(input); | |
807 fragment.appendChild(this.ownerDocument.createTextNode(name)); | |
808 return fragment; | |
809 }; | |
810 | |
811 /** | |
812 * Renders the selection column header. | |
813 * @param {string} name Localized column name. | |
814 * @return {HTMLLiElement} Created element. | |
815 * @private | |
816 */ | |
817 FileTable.prototype.renderSelectionColumnHeader_ = function(name) { | |
818 if (!this.selectionModel.multiple) | |
819 return this.ownerDocument.createTextNode(''); | |
820 | |
821 var input = this.ownerDocument.createElement('input'); | |
822 input.setAttribute('type', 'checkbox'); | |
823 input.setAttribute('tabindex', -1); | |
824 input.id = 'select-all-checkbox'; | |
825 input.className = 'common'; | |
826 | |
827 this.updateSelectAllCheckboxState_(input); | |
828 | |
829 input.addEventListener('click', function(event) { | |
830 if (input.checked) | |
831 this.selectionModel.selectAll(); | |
832 else | |
833 this.selectionModel.unselectAll(); | |
834 event.stopPropagation(); | |
835 }.bind(this)); | |
836 | |
837 var fragment = this.ownerDocument.createDocumentFragment(); | |
838 fragment.appendChild(input); | |
839 return fragment; | |
840 }; | |
841 | |
842 /** | |
843 * Render the type column of the detail table. | |
844 * | |
845 * Invoked by cr.ui.Table when a file needs to be rendered. | |
846 * | |
847 * @param {Entry} entry The Entry object to render. | |
848 * @param {string} columnId The id of the column to be rendered. | |
849 * @param {cr.ui.Table} table The table doing the rendering. | |
850 * @return {HTMLDivElement} Created element. | |
851 * @private | |
852 */ | |
853 FileTable.prototype.renderIconType_ = function(entry, columnId, table) { | |
854 var icon = this.ownerDocument.createElement('div'); | |
855 icon.className = 'detail-icon'; | |
856 icon.setAttribute('file-type-icon', FileType.getIcon(entry)); | |
857 return icon; | |
858 }; | |
859 | |
860 /** | |
861 * Sets the margin height for the transparent preview panel at the bottom. | |
862 * @param {number} margin Margin to be set in px. | |
863 */ | |
864 FileTable.prototype.setBottomMarginForPanel = function(margin) { | |
865 this.list_.style.paddingBottom = margin + 'px'; | |
866 this.scrollBar_.setBottomMarginForPanel(margin); | |
867 }; | |
868 | |
869 /** | |
870 * Redraws the UI. Skips multiple consecutive calls. | |
871 */ | |
872 FileTable.prototype.relayout = function() { | |
873 this.relayoutAggregation_.run(); | |
874 }; | |
875 | |
876 /** | |
877 * Redraws the UI immediately. | |
878 * @private | |
879 */ | |
880 FileTable.prototype.relayoutImmediately_ = function() { | |
881 if (this.clientWidth > 0) | |
882 this.normalizeColumns(); | |
883 this.redraw(); | |
884 cr.dispatchSimpleEvent(this.list, 'relayout'); | |
885 }; | |
886 | |
887 /** | |
888 * Decorates (and wire up) a checkbox to be used in either a detail or a | |
889 * thumbnail list item. | |
890 * @param {HTMLInputElement} input Element to decorate. | |
891 */ | |
892 filelist.decorateCheckbox = function(input) { | |
893 var stopEventPropagation = function(event) { | |
894 if (!event.shiftKey) | |
895 event.stopPropagation(); | |
896 }; | |
897 input.setAttribute('type', 'checkbox'); | |
898 input.setAttribute('tabindex', -1); | |
899 input.classList.add('common'); | |
900 input.addEventListener('mousedown', stopEventPropagation); | |
901 input.addEventListener('mouseup', stopEventPropagation); | |
902 | |
903 input.addEventListener( | |
904 'click', | |
905 /** | |
906 * @this {HTMLInputElement} | |
907 */ | |
908 function(event) { | |
909 // Revert default action and swallow the event | |
910 // if this is a multiple click or Shift is pressed. | |
911 if (event.detail > 1 || event.shiftKey) { | |
912 this.checked = !this.checked; | |
913 stopEventPropagation(event); | |
914 } | |
915 }); | |
916 }; | |
917 | |
918 /** | |
919 * Decorates selection checkbox. | |
920 * @param {HTMLInputElement} input Element to decorate. | |
921 * @param {Entry} entry Corresponding entry. | |
922 * @param {cr.ui.List} list Owner list. | |
923 */ | |
924 filelist.decorateSelectionCheckbox = function(input, entry, list) { | |
925 filelist.decorateCheckbox(input); | |
926 input.classList.add('file-checkbox'); | |
927 input.addEventListener('click', function(e) { | |
928 var sm = list.selectionModel; | |
929 var listIndex = list.getListItemAncestor(this).listIndex; | |
930 sm.setIndexSelected(listIndex, this.checked); | |
931 sm.leadIndex = listIndex; | |
932 if (sm.anchorIndex == -1) | |
933 sm.anchorIndex = listIndex; | |
934 | |
935 }); | |
936 // Since we do not want to open the item when tap on checkbox, we need to | |
937 // stop propagation of TAP event dispatched by checkbox ideally. But all | |
938 // touch events from touch_handler are dispatched to the list control. So we | |
939 // have to stop propagation of native touchstart event to prevent list | |
940 // control from generating TAP event here. The synthetic click event will | |
941 // select the touched checkbox/item. | |
942 input.addEventListener('touchstart', | |
943 function(e) { e.stopPropagation() }); | |
944 | |
945 var index = list.dataModel.indexOf(entry); | |
946 // Our DOM nodes get discarded as soon as we're scrolled out of view, | |
947 // so we have to make sure the check state is correct when we're brought | |
948 // back to life. | |
949 input.checked = list.selectionModel.getIndexSelected(index); | |
950 }; | |
951 | |
952 /** | |
953 * Common item decoration for table's and grid's items. | |
954 * @param {ListItem} li List item. | |
955 * @param {Entry} entry The entry. | |
956 * @param {MetadataCache} metadataCache Cache to retrieve metadada. | |
957 */ | |
958 filelist.decorateListItem = function(li, entry, metadataCache) { | |
959 li.classList.add(entry.isDirectory ? 'directory' : 'file'); | |
960 if (FileType.isOnDrive(entry)) { | |
961 // The metadata may not yet be ready. In that case, the list item will be | |
962 // updated when the metadata is ready via updateListItemsMetadata. | |
963 var driveProps = metadataCache.getCached(entry, 'drive'); | |
964 if (driveProps) | |
965 filelist.updateListItemDriveProps(li, driveProps); | |
966 } | |
967 | |
968 // Overriding the default role 'list' to 'listbox' for better | |
969 // accessibility on ChromeOS. | |
970 li.setAttribute('role', 'option'); | |
971 | |
972 Object.defineProperty(li, 'selected', { | |
973 /** | |
974 * @this {ListItem} | |
975 * @return {boolean} True if the list item is selected. | |
976 */ | |
977 get: function() { | |
978 return this.hasAttribute('selected'); | |
979 }, | |
980 | |
981 /** | |
982 * @this {ListItem} | |
983 */ | |
984 set: function(v) { | |
985 if (v) | |
986 this.setAttribute('selected'); | |
987 else | |
988 this.removeAttribute('selected'); | |
989 var checkBox = this.querySelector('input.file-checkbox'); | |
990 if (checkBox) | |
991 checkBox.checked = !!v; | |
992 } | |
993 }); | |
994 }; | |
995 | |
996 /** | |
997 * Render filename label for grid and list view. | |
998 * @param {HTMLDocument} doc Owner document. | |
999 * @param {Entry} entry The Entry object to render. | |
1000 * @return {HTMLDivElement} The label. | |
1001 */ | |
1002 filelist.renderFileNameLabel = function(doc, entry) { | |
1003 // Filename need to be in a '.filename-label' container for correct | |
1004 // work of inplace renaming. | |
1005 var box = doc.createElement('div'); | |
1006 box.className = 'filename-label'; | |
1007 var fileName = doc.createElement('span'); | |
1008 fileName.textContent = entry.name; | |
1009 box.appendChild(fileName); | |
1010 | |
1011 return box; | |
1012 }; | |
1013 | |
1014 /** | |
1015 * Updates grid item or table row for the driveProps. | |
1016 * @param {cr.ui.ListItem} li List item. | |
1017 * @param {Object} driveProps Metadata. | |
1018 */ | |
1019 filelist.updateListItemDriveProps = function(li, driveProps) { | |
1020 if (li.classList.contains('file')) { | |
1021 if (driveProps.availableOffline) | |
1022 li.classList.remove('dim-offline'); | |
1023 else | |
1024 li.classList.add('dim-offline'); | |
1025 // TODO(mtomasz): Consider adding some vidual indication for files which | |
1026 // are not cached on LTE. Currently we show them as normal files. | |
1027 // crbug.com/246611. | |
1028 } | |
1029 | |
1030 if (driveProps.customIconUrl) { | |
1031 var iconDiv = li.querySelector('.detail-icon'); | |
1032 if (!iconDiv) | |
1033 return; | |
1034 iconDiv.style.backgroundImage = 'url(' + driveProps.customIconUrl + ')'; | |
1035 } | |
1036 }; | |
OLD | NEW |