Home | History | Annotate | Download | only in 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   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 positon.
    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 };
    368 
    369 /**
    370  * Sets date and time format.
    371  * @param {boolean} use12hourClock True if 12 hours clock, False if 24 hours.
    372  */
    373 FileTable.prototype.setDateTimeFormat = function(use12hourClock) {
    374   this.timeFormatter_ = Intl.DateTimeFormat(
    375         [] /* default locale */,
    376         {hour: 'numeric', minute: 'numeric',
    377          hour12: use12hourClock});
    378   this.dateFormatter_ = Intl.DateTimeFormat(
    379         [] /* default locale */,
    380         {year: 'numeric', month: 'short', day: 'numeric',
    381          hour: 'numeric', minute: 'numeric',
    382          hour12: use12hourClock});
    383 };
    384 
    385 /**
    386  * Obtains if the drag selection should be start or not by referring the mouse
    387  * event.
    388  * @param {MouseEvent} event Drag start event.
    389  * @return {boolean} True if the mouse is hit to the background of the list.
    390  */
    391 FileTable.prototype.shouldStartDragSelection = function(event) {
    392   // If the shift key is pressed, it should starts drag selection.
    393   if (event.shiftKey)
    394     return true;
    395 
    396   // If the position values are negative, it points the out of list.
    397   // It should start the drag selection.
    398   var pos = DragSelector.getScrolledPosition(this.list, event);
    399   if (!pos)
    400     return false;
    401   if (pos.x < 0 || pos.y < 0)
    402     return true;
    403 
    404   // If the item index is out of range, it should start the drag selection.
    405   var itemHeight = this.list.measureItem().height;
    406   // Faster alternative to Math.floor for non-negative numbers.
    407   var itemIndex = ~~(pos.y / itemHeight);
    408   if (itemIndex >= this.list.dataModel.length)
    409     return true;
    410 
    411   // If the pointed item is already selected, it should not start the drag
    412   // selection.
    413   if (this.lastSelection_.indexOf(itemIndex) != -1)
    414     return false;
    415 
    416   // If the horizontal value is not hit to column, it shoud start the drag
    417   // selection.
    418   var hitColumn = this.columnModel.getHitColumn(pos.x);
    419   if (!hitColumn)
    420     return true;
    421 
    422   // Check if the point is on the column contents or not.
    423   var item = this.list.getListItemByIndex(itemIndex);
    424   switch (this.columnModel.columns_[hitColumn.index].id) {
    425     case 'name':
    426       var spanElement = item.querySelector('.filename-label span');
    427       var spanRect = spanElement.getBoundingClientRect();
    428       // The this.list.cachedBounds_ object is set by
    429       // DragSelector.getScrolledPosition.
    430       if (!this.list.cachedBounds)
    431         return true;
    432       var textRight =
    433           spanRect.left - this.list.cachedBounds.left + spanRect.width;
    434       return textRight <= hitColumn.hitPosition;
    435     default:
    436       return true;
    437   }
    438 };
    439 
    440 /**
    441  * Update check and disable states of the 'Select all' checkbox.
    442  * @param {HTMLInputElement} checkbox The checkbox. If not passed, using
    443  *     the default one.
    444  * @private
    445  */
    446 FileTable.prototype.updateSelectAllCheckboxState_ = function(checkbox) {
    447   // TODO(serya): introduce this.selectionModel.selectedCount.
    448   checkbox.checked = this.dataModel.length > 0 &&
    449       this.dataModel.length == this.selectionModel.selectedIndexes.length;
    450   checkbox.disabled = this.dataModel.length == 0;
    451 };
    452 
    453 /**
    454  * Prepares the data model to be sorted by columns.
    455  * @param {cr.ui.ArrayDataModel} dataModel Data model to prepare.
    456  */
    457 FileTable.prototype.setupCompareFunctions = function(dataModel) {
    458   dataModel.setCompareFunction('name',
    459                                this.compareName_.bind(this));
    460   dataModel.setCompareFunction('modificationTime',
    461                                this.compareMtime_.bind(this));
    462   dataModel.setCompareFunction('size',
    463                                this.compareSize_.bind(this));
    464   dataModel.setCompareFunction('type',
    465                                this.compareType_.bind(this));
    466 };
    467 
    468 /**
    469  * Render the Name column of the detail table.
    470  *
    471  * Invoked by cr.ui.Table when a file needs to be rendered.
    472  *
    473  * @param {Entry} entry The Entry object to render.
    474  * @param {string} columnId The id of the column to be rendered.
    475  * @param {cr.ui.Table} table The table doing the rendering.
    476  * @return {HTMLDivElement} Created element.
    477  * @private
    478  */
    479 FileTable.prototype.renderName_ = function(entry, columnId, table) {
    480   var label = this.ownerDocument.createElement('div');
    481   label.appendChild(this.renderIconType_(entry, columnId, table));
    482   label.entry = entry;
    483   label.className = 'detail-name';
    484   label.appendChild(filelist.renderFileNameLabel(this.ownerDocument, entry));
    485   return label;
    486 };
    487 
    488 /**
    489  * Render the Selection column of the detail table.
    490  *
    491  * Invoked by cr.ui.Table when a file needs to be rendered.
    492  *
    493  * @param {Entry} entry The Entry object to render.
    494  * @param {string} columnId The id of the column to be rendered.
    495  * @param {cr.ui.Table} table The table doing the rendering.
    496  * @return {HTMLDivElement} Created element.
    497  * @private
    498  */
    499 FileTable.prototype.renderSelection_ = function(entry, columnId, table) {
    500   var label = this.ownerDocument.createElement('div');
    501   label.className = 'selection-label';
    502   if (this.selectionModel.multiple) {
    503     var checkBox = this.ownerDocument.createElement('input');
    504     filelist.decorateSelectionCheckbox(checkBox, entry, this.list);
    505     label.appendChild(checkBox);
    506   }
    507   return label;
    508 };
    509 
    510 /**
    511  * Render the Size column of the detail table.
    512  *
    513  * @param {Entry} entry The Entry object to render.
    514  * @param {string} columnId The id of the column to be rendered.
    515  * @param {cr.ui.Table} table The table doing the rendering.
    516  * @return {HTMLDivElement} Created element.
    517  * @private
    518  */
    519 FileTable.prototype.renderSize_ = function(entry, columnId, table) {
    520   var div = this.ownerDocument.createElement('div');
    521   div.className = 'size';
    522   this.updateSize_(
    523       div, entry, this.metadataCache_.getCached(entry, 'filesystem'));
    524 
    525   return div;
    526 };
    527 
    528 /**
    529  * Sets up or updates the size cell.
    530  *
    531  * @param {HTMLDivElement} div The table cell.
    532  * @param {Entry} entry The corresponding entry.
    533  * @param {Object} filesystemProps Metadata.
    534  * @private
    535  */
    536 FileTable.prototype.updateSize_ = function(div, entry, filesystemProps) {
    537   if (!filesystemProps) {
    538     div.textContent = '...';
    539   } else if (filesystemProps.size == -1) {
    540     div.textContent = '--';
    541   } else if (filesystemProps.size == 0 &&
    542              FileType.isHosted(entry)) {
    543     div.textContent = '--';
    544   } else {
    545     div.textContent = util.bytesToString(filesystemProps.size);
    546   }
    547 };
    548 
    549 /**
    550  * Render the Type column of the detail table.
    551  *
    552  * @param {Entry} entry The Entry object to render.
    553  * @param {string} columnId The id of the column to be rendered.
    554  * @param {cr.ui.Table} table The table doing the rendering.
    555  * @return {HTMLDivElement} Created element.
    556  * @private
    557  */
    558 FileTable.prototype.renderType_ = function(entry, columnId, table) {
    559   var div = this.ownerDocument.createElement('div');
    560   div.className = 'type';
    561   div.textContent = FileType.getTypeString(entry);
    562   return div;
    563 };
    564 
    565 /**
    566  * Render the Date column of the detail table.
    567  *
    568  * @param {Entry} entry The Entry object to render.
    569  * @param {string} columnId The id of the column to be rendered.
    570  * @param {cr.ui.Table} table The table doing the rendering.
    571  * @return {HTMLDivElement} Created element.
    572  * @private
    573  */
    574 FileTable.prototype.renderDate_ = function(entry, columnId, table) {
    575   var div = this.ownerDocument.createElement('div');
    576   div.className = 'date';
    577 
    578   this.updateDate_(div,
    579       this.metadataCache_.getCached(entry, 'filesystem'));
    580   return div;
    581 };
    582 
    583 /**
    584  * Sets up or updates the date cell.
    585  *
    586  * @param {HTMLDivElement} div The table cell.
    587  * @param {Object} filesystemProps Metadata.
    588  * @private
    589  */
    590 FileTable.prototype.updateDate_ = function(div, filesystemProps) {
    591   if (!filesystemProps) {
    592     div.textContent = '...';
    593     return;
    594   }
    595 
    596   var modTime = filesystemProps.modificationTime;
    597   var today = new Date();
    598   today.setHours(0);
    599   today.setMinutes(0);
    600   today.setSeconds(0);
    601   today.setMilliseconds(0);
    602 
    603   /**
    604    * Number of milliseconds in a day.
    605    */
    606   var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
    607 
    608   if (modTime >= today &&
    609       modTime < today.getTime() + MILLISECONDS_IN_DAY) {
    610     div.textContent = strf('TIME_TODAY', this.timeFormatter_.format(modTime));
    611   } else if (modTime >= today - MILLISECONDS_IN_DAY && modTime < today) {
    612     div.textContent = strf('TIME_YESTERDAY',
    613                            this.timeFormatter_.format(modTime));
    614   } else {
    615     div.textContent =
    616         this.dateFormatter_.format(filesystemProps.modificationTime);
    617   }
    618 };
    619 
    620 /**
    621  * Updates the file metadata in the table item.
    622  *
    623  * @param {Element} item Table item.
    624  * @param {Entry} entry File entry.
    625  */
    626 FileTable.prototype.updateFileMetadata = function(item, entry) {
    627   var props = this.metadataCache_.getCached(entry, 'filesystem');
    628   this.updateDate_(item.querySelector('.date'), props);
    629   this.updateSize_(item.querySelector('.size'), entry, props);
    630 };
    631 
    632 /**
    633  * Updates list items 'in place' on metadata change.
    634  * @param {string} type Type of metadata change.
    635  * @param {Object.<sting, Object>} propsMap Map from entry URLs to metadata
    636  *                                          properties.
    637  */
    638 FileTable.prototype.updateListItemsMetadata = function(type, propsMap) {
    639   var forEachCell = function(selector, callback) {
    640     var cells = this.querySelectorAll(selector);
    641     for (var i = 0; i < cells.length; i++) {
    642       var cell = cells[i];
    643       var listItem = this.list_.getListItemAncestor(cell);
    644       var entry = this.dataModel.item(listItem.listIndex);
    645       if (entry) {
    646         var props = propsMap[entry.toURL()];
    647         if (props)
    648           callback.call(this, cell, entry, props, listItem);
    649       }
    650     }
    651   }.bind(this);
    652   if (type == 'filesystem') {
    653     forEachCell('.table-row-cell > .date', function(item, entry, props) {
    654       this.updateDate_(item, props);
    655     });
    656     forEachCell('.table-row-cell > .size', function(item, entry, props) {
    657       this.updateSize_(item, entry, props);
    658     });
    659   } else if (type == 'drive') {
    660     forEachCell('.table-row-cell > .offline',
    661                 function(item, entry, props, listItem) {
    662       this.updateOffline_(item, props);
    663       filelist.updateListItemDriveProps(listItem, props);
    664     });
    665   }
    666 };
    667 
    668 /**
    669  * Compare by mtime first, then by name.
    670  * @param {Entry} a First entry.
    671  * @param {Entry} b Second entry.
    672  * @return {number} Compare result.
    673  * @private
    674  */
    675 FileTable.prototype.compareName_ = function(a, b) {
    676   return this.collator_.compare(a.name, b.name);
    677 };
    678 
    679 /**
    680  * Compare by mtime first, then by name.
    681  * @param {Entry} a First entry.
    682  * @param {Entry} b Second entry.
    683  * @return {number} Compare result.
    684  * @private
    685  */
    686 FileTable.prototype.compareMtime_ = function(a, b) {
    687   var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
    688   var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0;
    689 
    690   var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
    691   var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0;
    692 
    693   if (aTime > bTime)
    694     return 1;
    695 
    696   if (aTime < bTime)
    697     return -1;
    698 
    699   return this.collator_.compare(a.name, b.name);
    700 };
    701 
    702 /**
    703  * Compare by size first, then by name.
    704  * @param {Entry} a First entry.
    705  * @param {Entry} b Second entry.
    706  * @return {number} Compare result.
    707  * @private
    708  */
    709 FileTable.prototype.compareSize_ = function(a, b) {
    710   var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
    711   var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0;
    712 
    713   var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
    714   var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0;
    715 
    716   if (aSize != bSize) return aSize - bSize;
    717     return this.collator_.compare(a.name, b.name);
    718 };
    719 
    720 /**
    721  * Compare by type first, then by subtype and then by name.
    722  * @param {Entry} a First entry.
    723  * @param {Entry} b Second entry.
    724  * @return {number} Compare result.
    725  * @private
    726  */
    727 FileTable.prototype.compareType_ = function(a, b) {
    728   // Directories precede files.
    729   if (a.isDirectory != b.isDirectory)
    730     return Number(b.isDirectory) - Number(a.isDirectory);
    731 
    732   var aType = FileType.getTypeString(a);
    733   var bType = FileType.getTypeString(b);
    734 
    735   var result = this.collator_.compare(aType, bType);
    736   if (result != 0)
    737     return result;
    738 
    739   return this.collator_.compare(a.name, b.name);
    740 };
    741 
    742 /**
    743  * Renders table row.
    744  * @param {function(Entry, cr.ui.Table)} baseRenderFunction Base renderer.
    745  * @param {Entry} entry Corresponding entry.
    746  * @return {HTMLLiElement} Created element.
    747  * @private
    748  */
    749 FileTable.prototype.renderTableRow_ = function(baseRenderFunction, entry) {
    750   var item = baseRenderFunction(entry, this);
    751   filelist.decorateListItem(item, entry, this.metadataCache_);
    752   return item;
    753 };
    754 
    755 /**
    756  * Renders the name column header.
    757  * @param {string} name Localized column name.
    758  * @return {HTMLLiElement} Created element.
    759  * @private
    760  */
    761 FileTable.prototype.renderNameColumnHeader_ = function(name) {
    762   if (!this.selectionModel.multiple)
    763     return this.ownerDocument.createTextNode(name);
    764 
    765   var input = this.ownerDocument.createElement('input');
    766   input.setAttribute('type', 'checkbox');
    767   input.setAttribute('tabindex', -1);
    768   input.id = 'select-all-checkbox';
    769   input.className = 'common';
    770 
    771   this.updateSelectAllCheckboxState_(input);
    772 
    773   input.addEventListener('click', function(event) {
    774     if (input.checked)
    775       this.selectionModel.selectAll();
    776     else
    777       this.selectionModel.unselectAll();
    778     event.stopPropagation();
    779   }.bind(this));
    780 
    781   var fragment = this.ownerDocument.createDocumentFragment();
    782   fragment.appendChild(input);
    783   fragment.appendChild(this.ownerDocument.createTextNode(name));
    784   return fragment;
    785 };
    786 
    787 /**
    788  * Renders the selection column header.
    789  * @param {string} name Localized column name.
    790  * @return {HTMLLiElement} Created element.
    791  * @private
    792  */
    793 FileTable.prototype.renderSelectionColumnHeader_ = function(name) {
    794   if (!this.selectionModel.multiple)
    795     return this.ownerDocument.createTextNode('');
    796 
    797   var input = this.ownerDocument.createElement('input');
    798   input.setAttribute('type', 'checkbox');
    799   input.setAttribute('tabindex', -1);
    800   input.id = 'select-all-checkbox';
    801   input.className = 'common';
    802 
    803   this.updateSelectAllCheckboxState_(input);
    804 
    805   input.addEventListener('click', function(event) {
    806     if (input.checked)
    807       this.selectionModel.selectAll();
    808     else
    809       this.selectionModel.unselectAll();
    810     event.stopPropagation();
    811   }.bind(this));
    812 
    813   var fragment = this.ownerDocument.createDocumentFragment();
    814   fragment.appendChild(input);
    815   return fragment;
    816 };
    817 
    818 /**
    819  * Render the type column of the detail table.
    820  *
    821  * Invoked by cr.ui.Table when a file needs to be rendered.
    822  *
    823  * @param {Entry} entry The Entry object to render.
    824  * @param {string} columnId The id of the column to be rendered.
    825  * @param {cr.ui.Table} table The table doing the rendering.
    826  * @return {HTMLDivElement} Created element.
    827  * @private
    828  */
    829 FileTable.prototype.renderIconType_ = function(entry, columnId, table) {
    830   var icon = this.ownerDocument.createElement('div');
    831   icon.className = 'detail-icon';
    832   icon.setAttribute('file-type-icon', FileType.getIcon(entry));
    833   return icon;
    834 };
    835 
    836 /**
    837  * Sets the margin height for the transparent preview panel at the bottom.
    838  * @param {number} margin Margin to be set in px.
    839  */
    840 FileTable.prototype.setBottomMarginForPanel = function(margin) {
    841   this.list_.style.paddingBottom = margin + 'px';
    842   this.scrollBar_.setBottomMarginForPanel(margin);
    843 };
    844 
    845 /**
    846  * Redraws the UI. Skips multiple consecutive calls.
    847  */
    848 FileTable.prototype.relayout = function() {
    849   this.relayoutAggregation_.run();
    850 };
    851 
    852 /**
    853  * Redraws the UI immediately.
    854  * @private
    855  */
    856 FileTable.prototype.relayoutImmediately_ = function() {
    857   if (this.clientWidth > 0)
    858     this.normalizeColumns();
    859   this.redraw();
    860   cr.dispatchSimpleEvent(this.list, 'relayout');
    861 };
    862 
    863 /**
    864  * Decorates (and wire up) a checkbox to be used in either a detail or a
    865  * thumbnail list item.
    866  * @param {HTMLInputElement} input Element to decorate.
    867  */
    868 filelist.decorateCheckbox = function(input) {
    869   var stopEventPropagation = function(event) {
    870     if (!event.shiftKey)
    871       event.stopPropagation();
    872   };
    873   input.setAttribute('type', 'checkbox');
    874   input.setAttribute('tabindex', -1);
    875   input.classList.add('common');
    876   input.addEventListener('mousedown', stopEventPropagation);
    877   input.addEventListener('mouseup', stopEventPropagation);
    878 
    879   input.addEventListener(
    880       'click',
    881       /**
    882        * @this {HTMLInputElement}
    883        */
    884       function(event) {
    885         // Revert default action and swallow the event
    886         // if this is a multiple click or Shift is pressed.
    887         if (event.detail > 1 || event.shiftKey) {
    888           this.checked = !this.checked;
    889           stopEventPropagation(event);
    890         }
    891       });
    892 };
    893 
    894 /**
    895  * Decorates selection checkbox.
    896  * @param {HTMLInputElement} input Element to decorate.
    897  * @param {Entry} entry Corresponding entry.
    898  * @param {cr.ui.List} list Owner list.
    899  */
    900 filelist.decorateSelectionCheckbox = function(input, entry, list) {
    901   filelist.decorateCheckbox(input);
    902   input.classList.add('file-checkbox');
    903   input.addEventListener('click', function(e) {
    904     var sm = list.selectionModel;
    905     var listIndex = list.getListItemAncestor(this).listIndex;
    906     sm.setIndexSelected(listIndex, this.checked);
    907     sm.leadIndex = listIndex;
    908     if (sm.anchorIndex == -1)
    909       sm.anchorIndex = listIndex;
    910 
    911   });
    912   // Since we do not want to open the item when tap on checkbox, we need to
    913   // stop propagation of TAP event dispatched by checkbox ideally. But all
    914   // touch events from touch_handler are dispatched to the list control. So we
    915   // have to stop propagation of native touchstart event to prevent list
    916   // control from generating TAP event here. The synthetic click event will
    917   // select the touched checkbox/item.
    918   input.addEventListener('touchstart',
    919                          function(e) { e.stopPropagation() });
    920 
    921   var index = list.dataModel.indexOf(entry);
    922   // Our DOM nodes get discarded as soon as we're scrolled out of view,
    923   // so we have to make sure the check state is correct when we're brought
    924   // back to life.
    925   input.checked = list.selectionModel.getIndexSelected(index);
    926 };
    927 
    928 /**
    929  * Common item decoration for table's and grid's items.
    930  * @param {ListItem} li List item.
    931  * @param {Entry} entry The entry.
    932  * @param {MetadataCache} metadataCache Cache to retrieve metadada.
    933  */
    934 filelist.decorateListItem = function(li, entry, metadataCache) {
    935   li.classList.add(entry.isDirectory ? 'directory' : 'file');
    936   if (FileType.isOnDrive(entry)) {
    937     var driveProps = metadataCache.getCached(entry, 'drive');
    938     if (driveProps)
    939       filelist.updateListItemDriveProps(li, driveProps);
    940   }
    941 
    942   // Overriding the default role 'list' to 'listbox' for better
    943   // accessibility on ChromeOS.
    944   li.setAttribute('role', 'option');
    945 
    946   Object.defineProperty(li, 'selected', {
    947     /**
    948      * @this {ListItem}
    949      * @return {boolean} True if the list item is selected.
    950      */
    951     get: function() {
    952       return this.hasAttribute('selected');
    953     },
    954 
    955     /**
    956      * @this {ListItem}
    957      */
    958     set: function(v) {
    959       if (v)
    960         this.setAttribute('selected');
    961       else
    962         this.removeAttribute('selected');
    963       var checkBox = this.querySelector('input.file-checkbox');
    964       if (checkBox)
    965         checkBox.checked = !!v;
    966     }
    967   });
    968 };
    969 
    970 /**
    971  * Render filename label for grid and list view.
    972  * @param {HTMLDocument} doc Owner document.
    973  * @param {Entry} entry The Entry object to render.
    974  * @return {HTMLDivElement} The label.
    975  */
    976 filelist.renderFileNameLabel = function(doc, entry) {
    977   // Filename need to be in a '.filename-label' container for correct
    978   // work of inplace renaming.
    979   var box = doc.createElement('div');
    980   box.className = 'filename-label';
    981   var fileName = doc.createElement('span');
    982   fileName.textContent = entry.name;
    983   box.appendChild(fileName);
    984 
    985   return box;
    986 };
    987 
    988 /**
    989  * Updates grid item or table row for the driveProps.
    990  * @param {cr.ui.ListItem} li List item.
    991  * @param {Object} driveProps Metadata.
    992  */
    993 filelist.updateListItemDriveProps = function(li, driveProps) {
    994   if (li.classList.contains('file')) {
    995     if (driveProps.availableOffline)
    996       li.classList.remove('dim-offline');
    997     else
    998       li.classList.add('dim-offline');
    999     // TODO(mtomasz): Consider adding some vidual indication for files which
   1000     // are not cached on LTE. Currently we show them as normal files.
   1001     // crbug.com/246611.
   1002   }
   1003 
   1004   if (driveProps.driveApps.length > 0) {
   1005     var iconDiv = li.querySelector('.detail-icon');
   1006     if (!iconDiv)
   1007       return;
   1008     // Find the default app for this file.  If there is none, then
   1009     // leave it as the base icon for the file type.
   1010     var url;
   1011     for (var i = 0; i < driveProps.driveApps.length; ++i) {
   1012       var app = driveProps.driveApps[i];
   1013       if (app && app.docIcon && app.isPrimary) {
   1014         url = app.docIcon;
   1015         break;
   1016       }
   1017     }
   1018     if (url) {
   1019       iconDiv.style.backgroundImage = 'url(' + url + ')';
   1020     } else {
   1021       iconDiv.style.backgroundImage = null;
   1022     }
   1023   }
   1024 };
   1025