Home | History | Annotate | Download | only in ui
      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 /**
      6  * @fileoverview This implements a table control.
      7  */
      8 
      9 cr.define('cr.ui', function() {
     10   /** @const */ var ListSelectionModel = cr.ui.ListSelectionModel;
     11   /** @const */ var ListSelectionController = cr.ui.ListSelectionController;
     12   /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
     13   /** @const */ var TableColumnModel = cr.ui.table.TableColumnModel;
     14   /** @const */ var TableList = cr.ui.table.TableList;
     15   /** @const */ var TableHeader = cr.ui.table.TableHeader;
     16 
     17   /**
     18    * Creates a new table element.
     19    * @param {Object=} opt_propertyBag Optional properties.
     20    * @constructor
     21    * @extends {HTMLDivElement}
     22    */
     23   var Table = cr.ui.define('div');
     24 
     25   Table.prototype = {
     26     __proto__: HTMLDivElement.prototype,
     27 
     28     columnModel_: new TableColumnModel([]),
     29 
     30     /**
     31      * The table data model.
     32      *
     33      * @type {cr.ui.ArrayDataModel}
     34      */
     35     get dataModel() {
     36       return this.list_.dataModel;
     37     },
     38     set dataModel(dataModel) {
     39       if (this.list_.dataModel != dataModel) {
     40         if (this.list_.dataModel) {
     41           this.list_.dataModel.removeEventListener('sorted',
     42                                                    this.boundHandleSorted_);
     43           this.list_.dataModel.removeEventListener('change',
     44                                                    this.boundHandleChangeList_);
     45           this.list_.dataModel.removeEventListener('splice',
     46                                                    this.boundHandleChangeList_);
     47         }
     48         this.list_.dataModel = dataModel;
     49         if (this.list_.dataModel) {
     50           this.list_.dataModel.addEventListener('sorted',
     51                                                 this.boundHandleSorted_);
     52           this.list_.dataModel.addEventListener('change',
     53                                                 this.boundHandleChangeList_);
     54           this.list_.dataModel.addEventListener('splice',
     55                                                 this.boundHandleChangeList_);
     56         }
     57         this.header_.redraw();
     58       }
     59     },
     60 
     61     /**
     62      * The list of table.
     63      *
     64      * @type {cr.ui.list}
     65      */
     66     get list() {
     67       return this.list_;
     68     },
     69 
     70     /**
     71      * The table column model.
     72      *
     73      * @type {cr.ui.table.TableColumnModel}
     74      */
     75     get columnModel() {
     76       return this.columnModel_;
     77     },
     78     set columnModel(columnModel) {
     79       if (this.columnModel_ != columnModel) {
     80         if (this.columnModel_)
     81           this.columnModel_.removeEventListener('resize', this.boundResize_);
     82         this.columnModel_ = columnModel;
     83 
     84         if (this.columnModel_)
     85           this.columnModel_.addEventListener('resize', this.boundResize_);
     86         this.list_.invalidate();
     87         this.redraw();
     88       }
     89     },
     90 
     91     /**
     92      * The table selection model.
     93      *
     94      * @type
     95      * {cr.ui.ListSelectionModel|cr.ui.table.ListSingleSelectionModel}
     96      */
     97     get selectionModel() {
     98       return this.list_.selectionModel;
     99     },
    100     set selectionModel(selectionModel) {
    101       if (this.list_.selectionModel != selectionModel) {
    102         if (this.dataModel)
    103           selectionModel.adjustLength(this.dataModel.length);
    104         this.list_.selectionModel = selectionModel;
    105       }
    106     },
    107 
    108     /**
    109      * The accessor to "autoExpands" property of the list.
    110      *
    111      * @type {boolean}
    112      */
    113     get autoExpands() {
    114       return this.list_.autoExpands;
    115     },
    116     set autoExpands(autoExpands) {
    117       this.list_.autoExpands = autoExpands;
    118     },
    119 
    120     get fixedHeight() {
    121       return this.list_.fixedHeight;
    122     },
    123     set fixedHeight(fixedHeight) {
    124       this.list_.fixedHeight = fixedHeight;
    125     },
    126 
    127     /**
    128      * Returns render function for row.
    129      * @return {Function(*, cr.ui.Table): HTMLElement} Render function.
    130      */
    131     getRenderFunction: function() {
    132       return this.list_.renderFunction_;
    133     },
    134 
    135     /**
    136      * Sets render function for row.
    137      * @param {Function(*, cr.ui.Table): HTMLElement} Render function.
    138      */
    139     setRenderFunction: function(renderFunction) {
    140       if (renderFunction === this.list_.renderFunction_)
    141         return;
    142 
    143       this.list_.renderFunction_ = renderFunction;
    144       cr.dispatchSimpleEvent(this, 'change');
    145     },
    146 
    147     /**
    148      * The header of the table.
    149      *
    150      * @type {cr.ui.table.TableColumnModel}
    151      */
    152     get header() {
    153       return this.header_;
    154     },
    155 
    156     /**
    157      * Initializes the element.
    158      */
    159     decorate: function() {
    160       this.header_ = this.ownerDocument.createElement('div');
    161       this.list_ = this.ownerDocument.createElement('list');
    162 
    163       this.appendChild(this.header_);
    164       this.appendChild(this.list_);
    165 
    166       TableList.decorate(this.list_);
    167       this.list_.selectionModel = new ListSelectionModel(this);
    168       this.list_.table = this;
    169       this.list_.addEventListener('scroll', this.handleScroll_.bind(this));
    170 
    171       TableHeader.decorate(this.header_);
    172       this.header_.table = this;
    173 
    174       this.classList.add('table');
    175 
    176       this.boundResize_ = this.resize.bind(this);
    177       this.boundHandleSorted_ = this.handleSorted_.bind(this);
    178       this.boundHandleChangeList_ = this.handleChangeList_.bind(this);
    179 
    180       // The contained list should be focusable, not the table itself.
    181       if (this.hasAttribute('tabindex')) {
    182         this.list_.setAttribute('tabindex', this.getAttribute('tabindex'));
    183         this.removeAttribute('tabindex');
    184       }
    185 
    186       this.addEventListener('focus', this.handleElementFocus_, true);
    187       this.addEventListener('blur', this.handleElementBlur_, true);
    188     },
    189 
    190     /**
    191      * Redraws the table.
    192      */
    193     redraw: function(index) {
    194       this.list_.redraw();
    195       this.header_.redraw();
    196     },
    197 
    198     startBatchUpdates: function() {
    199       this.list_.startBatchUpdates();
    200       this.header_.startBatchUpdates();
    201     },
    202 
    203     endBatchUpdates: function() {
    204       this.list_.endBatchUpdates();
    205       this.header_.endBatchUpdates();
    206     },
    207 
    208     /**
    209      * Resize the table columns.
    210      */
    211     resize: function() {
    212       // We resize columns only instead of full redraw.
    213       this.list_.resize();
    214       this.header_.resize();
    215     },
    216 
    217     /**
    218      * Ensures that a given index is inside the viewport.
    219      * @param {number} i The index of the item to scroll into view.
    220      * @return {boolean} Whether any scrolling was needed.
    221      */
    222     scrollIndexIntoView: function(i) {
    223       this.list_.scrollIndexIntoView(i);
    224     },
    225 
    226     /**
    227      * Find the list item element at the given index.
    228      * @param {number} index The index of the list item to get.
    229      * @return {ListItem} The found list item or null if not found.
    230      */
    231     getListItemByIndex: function(index) {
    232       return this.list_.getListItemByIndex(index);
    233     },
    234 
    235     /**
    236      * This handles data model 'sorted' event.
    237      * After sorting we need to redraw header
    238      * @param {Event} e The 'sorted' event.
    239      */
    240     handleSorted_: function(e) {
    241       this.header_.redraw();
    242     },
    243 
    244     /**
    245      * This handles data model 'change' and 'splice' events.
    246      * Since they may change the visibility of scrollbar, table may need to
    247      * re-calculation the width of column headers.
    248      * @param {Event} e The 'change' or 'splice' event.
    249      */
    250     handleChangeList_: function(e) {
    251       requestAnimationFrame(this.header_.updateWidth.bind(this.header_));
    252     },
    253 
    254     /**
    255      * This handles list 'scroll' events. Scrolls the header accordingly.
    256      * @param {Event} e Scroll event.
    257      */
    258     handleScroll_: function(e) {
    259       this.header_.style.marginLeft = -this.list_.scrollLeft + 'px';
    260     },
    261 
    262     /**
    263      * Sort data by the given column.
    264      * @param {number} i The index of the column to sort by.
    265      */
    266     sort: function(i) {
    267       var cm = this.columnModel_;
    268       var sortStatus = this.list_.dataModel.sortStatus;
    269       if (sortStatus.field == cm.getId(i)) {
    270         var sortDirection = sortStatus.direction == 'desc' ? 'asc' : 'desc';
    271         this.list_.dataModel.sort(sortStatus.field, sortDirection);
    272       } else {
    273         this.list_.dataModel.sort(cm.getId(i), cm.getDefaultOrder(i));
    274       }
    275       if (this.selectionModel.selectedIndex == -1)
    276         this.list_.scrollTop = 0;
    277     },
    278 
    279     /**
    280      * Called when an element in the table is focused. Marks the table as having
    281      * a focused element, and dispatches an event if it didn't have focus.
    282      * @param {Event} e The focus event.
    283      * @private
    284      */
    285     handleElementFocus_: function(e) {
    286       if (!this.hasElementFocus) {
    287         this.hasElementFocus = true;
    288         // Force styles based on hasElementFocus to take effect.
    289         this.list_.redraw();
    290       }
    291     },
    292 
    293     /**
    294      * Called when an element in the table is blurred. If focus moves outside
    295      * the table, marks the table as no longer having focus and dispatches an
    296      * event.
    297      * @param {Event} e The blur event.
    298      * @private
    299      */
    300     handleElementBlur_: function(e) {
    301       // When the blur event happens we do not know who is getting focus so we
    302       // delay this a bit until we know if the new focus node is outside the
    303       // table.
    304       var table = this;
    305       var list = this.list_;
    306       var doc = e.target.ownerDocument;
    307       window.setTimeout(function() {
    308         var activeElement = doc.activeElement;
    309         if (!table.contains(activeElement)) {
    310           table.hasElementFocus = false;
    311           // Force styles based on hasElementFocus to take effect.
    312           list.redraw();
    313         }
    314       });
    315     },
    316 
    317     /**
    318      * Adjust column width to fit its content.
    319      * @param {number} index Index of the column to adjust width.
    320      */
    321     fitColumn: function(index) {
    322       var list = this.list_;
    323       var listHeight = list.clientHeight;
    324 
    325       var cm = this.columnModel_;
    326       var dm = this.dataModel;
    327       var columnId = cm.getId(index);
    328       var doc = this.ownerDocument;
    329       var render = cm.getRenderFunction(index);
    330       var table = this;
    331       var MAXIMUM_ROWS_TO_MEASURE = 1000;
    332 
    333       // Create a temporaty list item, put all cells into it and measure its
    334       // width. Then remove the item. It fits "list > *" CSS rules.
    335       var container = doc.createElement('li');
    336       container.style.display = 'inline-block';
    337       container.style.textAlign = 'start';
    338       // The container will have width of the longest cell.
    339       container.style.webkitBoxOrient = 'vertical';
    340 
    341       // Ensure all needed data available.
    342       dm.prepareSort(columnId, function() {
    343         // Select at most MAXIMUM_ROWS_TO_MEASURE items around visible area.
    344         var items = list.getItemsInViewPort(list.scrollTop, listHeight);
    345         var firstIndex = Math.floor(Math.max(0,
    346             (items.last + items.first - MAXIMUM_ROWS_TO_MEASURE) / 2));
    347         var lastIndex = Math.min(dm.length,
    348                                  firstIndex + MAXIMUM_ROWS_TO_MEASURE);
    349         for (var i = firstIndex; i < lastIndex; i++) {
    350           var item = dm.item(i);
    351           var div = doc.createElement('div');
    352           div.className = 'table-row-cell';
    353           div.appendChild(render(item, columnId, table));
    354           container.appendChild(div);
    355         }
    356         list.appendChild(container);
    357         var width = parseFloat(window.getComputedStyle(container).width);
    358         list.removeChild(container);
    359         cm.setWidth(index, width);
    360       });
    361     },
    362 
    363     normalizeColumns: function() {
    364       this.columnModel.normalizeWidths(this.clientWidth);
    365     }
    366   };
    367 
    368   /**
    369    * Whether the table or one of its descendents has focus. This is necessary
    370    * because table contents can contain controls that can be focused, and for
    371    * some purposes (e.g., styling), the table can still be conceptually focused
    372    * at that point even though it doesn't actually have the page focus.
    373    */
    374   cr.defineProperty(Table, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);
    375 
    376   return {
    377     Table: Table
    378   };
    379 });
    380