Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / file_table.js
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
244   var columnModel = Object.create(tableColumnModelClass.prototype, {
245     /**
246      * The number of columns.
247      * @type {number}
248      */
249     size: {
250       /**
251        * @this {FileTableColumnModel}
252        * @return {number} Number of columns.
253        */
254       get: function() {
255         return this.totalSize;
256       }
257     },
258
259     /**
260      * The number of columns.
261      * @type {number}
262      */
263     totalSize: {
264       /**
265        * @this {FileTableColumnModel}
266        * @return {number} Number of columns.
267        */
268       get: function() {
269         return columns.length;
270       }
271     },
272
273     /**
274      * Obtains a column by the specified horizontal position.
275      */
276     getHitColumn: {
277       /**
278        * @this {FileTableColumnModel}
279        * @param {number} x Horizontal position.
280        * @return {object} The object that contains column index, column width,
281        *     and hitPosition where the horizontal position is hit in the column.
282        */
283       value: function(x) {
284         for (var i = 0; x >= this.columns_[i].width; i++) {
285           x -= this.columns_[i].width;
286         }
287         if (i >= this.columns_.length)
288           return null;
289         return {index: i, hitPosition: x, width: this.columns_[i].width};
290       }
291     }
292   });
293
294   tableColumnModelClass.call(columnModel, columns);
295   self.columnModel = columnModel;
296   self.setDateTimeFormat(true);
297   self.setRenderFunction(self.renderTableRow_.bind(self,
298       self.getRenderFunction()));
299
300   self.scrollBar_ = MainPanelScrollBar();
301   self.scrollBar_.initialize(self, self.list);
302   // Keep focus on the file list when clicking on the header.
303   self.header.addEventListener('mousedown', function(e) {
304     self.list.focus();
305     e.preventDefault();
306   });
307
308   self.relayoutAggregation_ =
309       new AsyncUtil.Aggregation(self.relayoutImmediately_.bind(self));
310
311   // Override header#redraw to use FileTableSplitter.
312   self.header_.redraw = function() {
313     this.__proto__.redraw.call(this);
314     // Extend table splitters
315     var splitters = this.querySelectorAll('.table-header-splitter');
316     for (var i = 0; i < splitters.length; i++) {
317       if (splitters[i] instanceof FileTableSplitter)
318         continue;
319       FileTableSplitter.decorate(splitters[i]);
320     }
321   };
322
323   // Save the last selection. This is used by shouldStartDragSelection.
324   self.list.addEventListener('mousedown', function(e) {
325     this.lastSelection_ = this.selectionModel.selectedIndexes;
326   }.bind(self), true);
327   self.list.shouldStartDragSelection =
328       self.shouldStartDragSelection_.bind(self);
329
330   /**
331    * Obtains the index list of elements that are hit by the point or the
332    * rectangle.
333    *
334    * @param {number} x X coordinate value.
335    * @param {number} y Y coordinate value.
336    * @param {=number} opt_width Width of the coordinate.
337    * @param {=number} opt_height Height of the coordinate.
338    * @return {Array.<number>} Index list of hit elements.
339    */
340   self.list.getHitElements = function(x, y, opt_width, opt_height) {
341     var currentSelection = [];
342     var bottom = y + (opt_height || 0);
343     for (var i = 0; i < this.selectionModel_.length; i++) {
344       var itemMetrics = this.getHeightsForIndex_(i);
345       if (itemMetrics.top < bottom && itemMetrics.top + itemMetrics.height >= y)
346         currentSelection.push(i);
347     }
348     return currentSelection;
349   };
350 };
351
352 /**
353  * Sets date and time format.
354  * @param {boolean} use12hourClock True if 12 hours clock, False if 24 hours.
355  */
356 FileTable.prototype.setDateTimeFormat = function(use12hourClock) {
357   this.timeFormatter_ = Intl.DateTimeFormat(
358         [] /* default locale */,
359         {hour: 'numeric', minute: 'numeric',
360          hour12: use12hourClock});
361   this.dateFormatter_ = Intl.DateTimeFormat(
362         [] /* default locale */,
363         {year: 'numeric', month: 'short', day: 'numeric',
364          hour: 'numeric', minute: 'numeric',
365          hour12: use12hourClock});
366 };
367
368 /**
369  * Obtains if the drag selection should be start or not by referring the mouse
370  * event.
371  * @param {MouseEvent} event Drag start event.
372  * @return {boolean} True if the mouse is hit to the background of the list.
373  * @private
374  */
375 FileTable.prototype.shouldStartDragSelection_ = function(event) {
376   // If the shift key is pressed, it should starts drag selection.
377   if (event.shiftKey)
378     return true;
379
380   // If the position values are negative, it points the out of list.
381   // It should start the drag selection.
382   var pos = DragSelector.getScrolledPosition(this.list, event);
383   if (!pos)
384     return false;
385   if (pos.x < 0 || pos.y < 0)
386     return true;
387
388   // If the item index is out of range, it should start the drag selection.
389   var itemHeight = this.list.measureItem().height;
390   // Faster alternative to Math.floor for non-negative numbers.
391   var itemIndex = ~~(pos.y / itemHeight);
392   if (itemIndex >= this.list.dataModel.length)
393     return true;
394
395   // If the pointed item is already selected, it should not start the drag
396   // selection.
397   if (this.lastSelection_.indexOf(itemIndex) !== -1)
398     return false;
399
400   // If the horizontal value is not hit to column, it should start the drag
401   // selection.
402   var hitColumn = this.columnModel.getHitColumn(pos.x);
403   if (!hitColumn)
404     return true;
405
406   // Check if the point is on the column contents or not.
407   var item = this.list.getListItemByIndex(itemIndex);
408   switch (this.columnModel.columns_[hitColumn.index].id) {
409     case 'name':
410       var spanElement = item.querySelector('.filename-label span');
411       var spanRect = spanElement.getBoundingClientRect();
412       // The this.list.cachedBounds_ object is set by
413       // DragSelector.getScrolledPosition.
414       if (!this.list.cachedBounds)
415         return true;
416       var textRight =
417           spanRect.left - this.list.cachedBounds.left + spanRect.width;
418       return textRight <= hitColumn.hitPosition;
419     default:
420       return true;
421   }
422 };
423
424 /**
425  * Prepares the data model to be sorted by columns.
426  * @param {cr.ui.ArrayDataModel} dataModel Data model to prepare.
427  */
428 FileTable.prototype.setupCompareFunctions = function(dataModel) {
429   dataModel.setCompareFunction('name',
430                                this.compareName_.bind(this));
431   dataModel.setCompareFunction('modificationTime',
432                                this.compareMtime_.bind(this));
433   dataModel.setCompareFunction('size',
434                                this.compareSize_.bind(this));
435   dataModel.setCompareFunction('type',
436                                this.compareType_.bind(this));
437 };
438
439 /**
440  * Render the Name column of the detail table.
441  *
442  * Invoked by cr.ui.Table when a file needs to be rendered.
443  *
444  * @param {Entry} entry The Entry object to render.
445  * @param {string} columnId The id of the column to be rendered.
446  * @param {cr.ui.Table} table The table doing the rendering.
447  * @return {HTMLDivElement} Created element.
448  * @private
449  */
450 FileTable.prototype.renderName_ = function(entry, columnId, table) {
451   var label = this.ownerDocument.createElement('div');
452   label.appendChild(this.renderIconType_(entry, columnId, table));
453   label.entry = entry;
454   label.className = 'detail-name';
455   label.appendChild(filelist.renderFileNameLabel(this.ownerDocument, entry));
456   return label;
457 };
458
459 /**
460  * Render the Size column of the detail table.
461  *
462  * @param {Entry} entry The Entry object to render.
463  * @param {string} columnId The id of the column to be rendered.
464  * @param {cr.ui.Table} table The table doing the rendering.
465  * @return {HTMLDivElement} Created element.
466  * @private
467  */
468 FileTable.prototype.renderSize_ = function(entry, columnId, table) {
469   var div = this.ownerDocument.createElement('div');
470   div.className = 'size';
471   this.updateSize_(
472       div, entry, this.metadataCache_.getCached(entry, 'filesystem'));
473
474   return div;
475 };
476
477 /**
478  * Sets up or updates the size cell.
479  *
480  * @param {HTMLDivElement} div The table cell.
481  * @param {Entry} entry The corresponding entry.
482  * @param {Object} filesystemProps Metadata.
483  * @private
484  */
485 FileTable.prototype.updateSize_ = function(div, entry, filesystemProps) {
486   if (!filesystemProps) {
487     div.textContent = '...';
488   } else if (filesystemProps.size === -1) {
489     div.textContent = '--';
490   } else if (filesystemProps.size === 0 &&
491              FileType.isHosted(entry)) {
492     div.textContent = '--';
493   } else {
494     div.textContent = util.bytesToString(filesystemProps.size);
495   }
496 };
497
498 /**
499  * Render the Type column of the detail table.
500  *
501  * @param {Entry} entry The Entry object to render.
502  * @param {string} columnId The id of the column to be rendered.
503  * @param {cr.ui.Table} table The table doing the rendering.
504  * @return {HTMLDivElement} Created element.
505  * @private
506  */
507 FileTable.prototype.renderType_ = function(entry, columnId, table) {
508   var div = this.ownerDocument.createElement('div');
509   div.className = 'type';
510   div.textContent = FileType.typeToString(FileType.getType(entry));
511   return div;
512 };
513
514 /**
515  * Render the Date column of the detail table.
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.renderDate_ = function(entry, columnId, table) {
524   var div = this.ownerDocument.createElement('div');
525   div.className = 'date';
526
527   this.updateDate_(div,
528       this.metadataCache_.getCached(entry, 'filesystem'));
529   return div;
530 };
531
532 /**
533  * Sets up or updates the date cell.
534  *
535  * @param {HTMLDivElement} div The table cell.
536  * @param {Object} filesystemProps Metadata.
537  * @private
538  */
539 FileTable.prototype.updateDate_ = function(div, filesystemProps) {
540   if (!filesystemProps) {
541     div.textContent = '...';
542     return;
543   }
544
545   var modTime = filesystemProps.modificationTime;
546   var today = new Date();
547   today.setHours(0);
548   today.setMinutes(0);
549   today.setSeconds(0);
550   today.setMilliseconds(0);
551
552   /**
553    * Number of milliseconds in a day.
554    */
555   var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
556
557   if (modTime >= today &&
558       modTime < today.getTime() + MILLISECONDS_IN_DAY) {
559     div.textContent = strf('TIME_TODAY', this.timeFormatter_.format(modTime));
560   } else if (modTime >= today - MILLISECONDS_IN_DAY && modTime < today) {
561     div.textContent = strf('TIME_YESTERDAY',
562                            this.timeFormatter_.format(modTime));
563   } else {
564     div.textContent =
565         this.dateFormatter_.format(filesystemProps.modificationTime);
566   }
567 };
568
569 /**
570  * Updates the file metadata in the table item.
571  *
572  * @param {Element} item Table item.
573  * @param {Entry} entry File entry.
574  */
575 FileTable.prototype.updateFileMetadata = function(item, entry) {
576   var props = this.metadataCache_.getCached(entry, 'filesystem');
577   this.updateDate_(item.querySelector('.date'), props);
578   this.updateSize_(item.querySelector('.size'), entry, props);
579 };
580
581 /**
582  * Updates list items 'in place' on metadata change.
583  * @param {string} type Type of metadata change.
584  * @param {Object.<string, Object>} propsMap Map from entry URLs to metadata
585  *     properties.
586  */
587 FileTable.prototype.updateListItemsMetadata = function(type, propsMap) {
588   var forEachCell = function(selector, callback) {
589     var cells = this.querySelectorAll(selector);
590     for (var i = 0; i < cells.length; i++) {
591       var cell = cells[i];
592       var listItem = this.list_.getListItemAncestor(cell);
593       var entry = this.dataModel.item(listItem.listIndex);
594       if (entry) {
595         var props = propsMap[entry.toURL()];
596         if (props)
597           callback.call(this, cell, entry, props, listItem);
598       }
599     }
600   }.bind(this);
601   if (type === 'filesystem') {
602     forEachCell('.table-row-cell > .date', function(item, entry, props) {
603       this.updateDate_(item, props);
604     });
605     forEachCell('.table-row-cell > .size', function(item, entry, props) {
606       this.updateSize_(item, entry, props);
607     });
608   } else if (type === 'drive') {
609     // The cell name does not matter as the entire list item is needed.
610     forEachCell('.table-row-cell > .date',
611                 function(item, entry, props, listItem) {
612       filelist.updateListItemDriveProps(listItem, props);
613     });
614   }
615 };
616
617 /**
618  * Compare by mtime first, then by name.
619  * @param {Entry} a First entry.
620  * @param {Entry} b Second entry.
621  * @return {number} Compare result.
622  * @private
623  */
624 FileTable.prototype.compareName_ = function(a, b) {
625   return this.collator_.compare(a.name, b.name);
626 };
627
628 /**
629  * Compare by mtime first, then by name.
630  * @param {Entry} a First entry.
631  * @param {Entry} b Second entry.
632  * @return {number} Compare result.
633  * @private
634  */
635 FileTable.prototype.compareMtime_ = function(a, b) {
636   var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
637   var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0;
638
639   var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
640   var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0;
641
642   if (aTime > bTime)
643     return 1;
644
645   if (aTime < bTime)
646     return -1;
647
648   return this.collator_.compare(a.name, b.name);
649 };
650
651 /**
652  * Compare by size first, then by name.
653  * @param {Entry} a First entry.
654  * @param {Entry} b Second entry.
655  * @return {number} Compare result.
656  * @private
657  */
658 FileTable.prototype.compareSize_ = function(a, b) {
659   var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
660   var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0;
661
662   var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
663   var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0;
664
665   if (aSize !== bSize) return aSize - bSize;
666     return this.collator_.compare(a.name, b.name);
667 };
668
669 /**
670  * Compare by type first, then by subtype and then by name.
671  * @param {Entry} a First entry.
672  * @param {Entry} b Second entry.
673  * @return {number} Compare result.
674  * @private
675  */
676 FileTable.prototype.compareType_ = function(a, b) {
677   // Directories precede files.
678   if (a.isDirectory !== b.isDirectory)
679     return Number(b.isDirectory) - Number(a.isDirectory);
680
681   var aType = FileType.typeToString(FileType.getType(a));
682   var bType = FileType.typeToString(FileType.getType(b));
683
684   var result = this.collator_.compare(aType, bType);
685   if (result !== 0)
686     return result;
687
688   return this.collator_.compare(a.name, b.name);
689 };
690
691 /**
692  * Renders table row.
693  * @param {function(Entry, cr.ui.Table)} baseRenderFunction Base renderer.
694  * @param {Entry} entry Corresponding entry.
695  * @return {HTMLLiElement} Created element.
696  * @private
697  */
698 FileTable.prototype.renderTableRow_ = function(baseRenderFunction, entry) {
699   var item = baseRenderFunction(entry, this);
700   filelist.decorateListItem(item, entry, this.metadataCache_);
701   return item;
702 };
703
704 /**
705  * Render the type column of the detail table.
706  *
707  * Invoked by cr.ui.Table when a file needs to be rendered.
708  *
709  * @param {Entry} entry The Entry object to render.
710  * @param {string} columnId The id of the column to be rendered.
711  * @param {cr.ui.Table} table The table doing the rendering.
712  * @return {HTMLDivElement} Created element.
713  * @private
714  */
715 FileTable.prototype.renderIconType_ = function(entry, columnId, table) {
716   var icon = this.ownerDocument.createElement('div');
717   icon.className = 'detail-icon';
718   icon.setAttribute('file-type-icon', FileType.getIcon(entry));
719   return icon;
720 };
721
722 /**
723  * Sets the margin height for the transparent preview panel at the bottom.
724  * @param {number} margin Margin to be set in px.
725  */
726 FileTable.prototype.setBottomMarginForPanel = function(margin) {
727   this.list_.style.paddingBottom = margin + 'px';
728   this.scrollBar_.setBottomMarginForPanel(margin);
729 };
730
731 /**
732  * Redraws the UI. Skips multiple consecutive calls.
733  */
734 FileTable.prototype.relayout = function() {
735   this.relayoutAggregation_.run();
736 };
737
738 /**
739  * Redraws the UI immediately.
740  * @private
741  */
742 FileTable.prototype.relayoutImmediately_ = function() {
743   if (this.clientWidth > 0)
744     this.normalizeColumns();
745   this.redraw();
746   cr.dispatchSimpleEvent(this.list, 'relayout');
747 };
748
749 /**
750  * Common item decoration for table's and grid's items.
751  * @param {ListItem} li List item.
752  * @param {Entry} entry The entry.
753  * @param {MetadataCache} metadataCache Cache to retrieve metadada.
754  */
755 filelist.decorateListItem = function(li, entry, metadataCache) {
756   li.classList.add(entry.isDirectory ? 'directory' : 'file');
757   // The metadata may not yet be ready. In that case, the list item will be
758   // updated when the metadata is ready via updateListItemsMetadata. For files
759   // not on Drive, driveProps is not available.
760   var driveProps = metadataCache.getCached(entry, 'drive');
761   if (driveProps)
762     filelist.updateListItemDriveProps(li, driveProps);
763
764   // Overriding the default role 'list' to 'listbox' for better
765   // accessibility on ChromeOS.
766   li.setAttribute('role', 'option');
767
768   Object.defineProperty(li, 'selected', {
769     /**
770      * @this {ListItem}
771      * @return {boolean} True if the list item is selected.
772      */
773     get: function() {
774       return this.hasAttribute('selected');
775     },
776
777     /**
778      * @this {ListItem}
779      */
780     set: function(v) {
781       if (v)
782         this.setAttribute('selected', '');
783       else
784         this.removeAttribute('selected');
785     }
786   });
787 };
788
789 /**
790  * Render filename label for grid and list view.
791  * @param {HTMLDocument} doc Owner document.
792  * @param {Entry} entry The Entry object to render.
793  * @return {HTMLDivElement} The label.
794  */
795 filelist.renderFileNameLabel = function(doc, entry) {
796   // Filename need to be in a '.filename-label' container for correct
797   // work of inplace renaming.
798   var box = doc.createElement('div');
799   box.className = 'filename-label';
800   var fileName = doc.createElement('span');
801   fileName.textContent = entry.name;
802   box.appendChild(fileName);
803
804   return box;
805 };
806
807 /**
808  * Updates grid item or table row for the driveProps.
809  * @param {cr.ui.ListItem} li List item.
810  * @param {Object} driveProps Metadata.
811  */
812 filelist.updateListItemDriveProps = function(li, driveProps) {
813   if (li.classList.contains('file')) {
814     if (driveProps.availableOffline)
815       li.classList.remove('dim-offline');
816     else
817       li.classList.add('dim-offline');
818     // TODO(mtomasz): Consider adding some vidual indication for files which
819     // are not cached on LTE. Currently we show them as normal files.
820     // crbug.com/246611.
821   }
822
823   if (driveProps.customIconUrl) {
824     var iconDiv = li.querySelector('.detail-icon');
825     if (!iconDiv)
826       return;
827     iconDiv.style.backgroundImage = 'url(' + driveProps.customIconUrl + ')';
828   }
829 };