Home | History | Annotate | Download | only in ui
      1 // Copyright (c) 2011 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 TableSelectionModel = cr.ui.table.TableSelectionModel;
     11   const ListSelectionController = cr.ui.ListSelectionController;
     12   const ArrayDataModel = cr.ui.ArrayDataModel;
     13   const TableColumnModel = cr.ui.table.TableColumnModel;
     14   const TableList = cr.ui.table.TableList;
     15   const 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.table.TableDataModel}
     34      */
     35     get dataModel() {
     36       return this.list_.dataModel;
     37     },
     38     set dataModel(dataModel) {
     39       if (this.list_.dataModel != dataModel) {
     40         this.list_.dataModel = dataModel;
     41         if (this.list_.dataModel) {
     42           this.list_.dataModel.removeEventListener('splice', this.boundRedraw_);
     43           this.list_.dataModel.removeEventListener('sorted',
     44                                                    this.boundHandleSorted_);
     45         }
     46         this.list_.dataModel = dataModel;
     47         this.list_.dataModel.table = this;
     48 
     49 
     50         if (this.list_.dataModel) {
     51           this.list_.dataModel.addEventListener('splice', this.boundRedraw_);
     52           this.list_.dataModel.addEventListener('sorted',
     53                                                 this.boundHandleSorted_);
     54         }
     55         this.header_.redraw();
     56       }
     57     },
     58 
     59     /**
     60      * The table column model.
     61      *
     62      * @type {cr.ui.table.TableColumnModel}
     63      */
     64     get columnModel() {
     65       return this.columnModel_;
     66     },
     67     set columnModel(columnModel) {
     68       if (this.columnModel_ != columnModel) {
     69         if (this.columnModel_) {
     70           this.columnModel_.removeEventListener('change', this.boundRedraw_);
     71           this.columnModel_.removeEventListener('resize', this.boundResize_);
     72         }
     73         this.columnModel_ = columnModel;
     74 
     75         if (this.columnModel_) {
     76           this.columnModel_.addEventListener('change', this.boundRedraw_);
     77           this.columnModel_.addEventListener('resize', this.boundResize_);
     78         }
     79         this.redraw();
     80       }
     81     },
     82 
     83     /**
     84      * The table selection model.
     85      *
     86      * @type
     87      * {cr.ui.table.TableSelectionModel|cr.ui.table.TableSingleSelectionModel}
     88      */
     89     get selectionModel() {
     90       return this.list_.selectionModel;
     91     },
     92     set selectionModel(selectionModel) {
     93       if (this.list_.selectionModel != selectionModel) {
     94         if (this.dataModel)
     95           selectionModel.adjust(0, 0, this.dataModel.length);
     96         this.list_.selectionModel = selectionModel;
     97         this.redraw();
     98       }
     99     },
    100 
    101     /**
    102      * Sets width of the column at the given index.
    103      *
    104      * @param {number} index The index of the column.
    105      * @param {number} Column width.
    106      */
    107     setColumnWidth: function(index, width) {
    108       this.columnWidths_[index] = width;
    109     },
    110 
    111     /**
    112      * Initializes the element.
    113      */
    114     decorate: function() {
    115       this.list_ = this.ownerDocument.createElement('list');
    116       TableList.decorate(this.list_);
    117       this.list_.selectionModel = new TableSelectionModel(this);
    118       this.list_.table = this;
    119 
    120       this.header_ = this.ownerDocument.createElement('div');
    121       TableHeader.decorate(this.header_);
    122       this.header_.table = this;
    123 
    124       this.classList.add('table');
    125       this.appendChild(this.header_);
    126       this.appendChild(this.list_);
    127       this.ownerDocument.defaultView.addEventListener(
    128           'resize', this.header_.updateWidth.bind(this.header_));
    129 
    130       this.boundRedraw_ = this.redraw.bind(this);
    131       this.boundResize_ = this.resize.bind(this);
    132       this.boundHandleSorted_ = this.handleSorted_.bind(this);
    133 
    134       // Make table focusable
    135       if (!this.hasAttribute('tabindex'))
    136         this.tabIndex = 0;
    137       this.addEventListener('focus', this.handleElementFocus_, true);
    138       this.addEventListener('blur', this.handleElementBlur_, true);
    139     },
    140 
    141     /**
    142      * Resize the table columns.
    143      */
    144     resize: function() {
    145       // We resize columns only instead of full redraw.
    146       this.list_.resize();
    147       this.header_.resize();
    148     },
    149 
    150     /**
    151      * Ensures that a given index is inside the viewport.
    152      * @param {number} index The index of the item to scroll into view.
    153      * @return {boolean} Whether any scrolling was needed.
    154      */
    155     scrollIndexIntoView: function(i) {
    156       this.list_.scrollIndexIntoView(i);
    157     },
    158 
    159     /**
    160      * Find the list item element at the given index.
    161      * @param {number} index The index of the list item to get.
    162      * @return {ListItem} The found list item or null if not found.
    163      */
    164     getListItemByIndex: function(index) {
    165       return this.list_.getListItemByIndex(index);
    166     },
    167 
    168     /**
    169      * Redraws the table.
    170      * This forces the list to remove all cached items.
    171      */
    172     redraw: function() {
    173       this.list_.startBatchUpdates();
    174       if (this.list_.dataModel) {
    175         for (var i = 0; i < this.list_.dataModel.length; i++) {
    176           this.list_.redrawItem(i);
    177         }
    178       }
    179       this.list_.endBatchUpdates();
    180       this.list_.redraw();
    181       this.header_.redraw();
    182     },
    183 
    184     /**
    185      * This handles data model 'sorted' event.
    186      * After sorting we need to
    187      *  - adjust selection
    188      *  - redraw all the items
    189      *  - scroll the list to show selection.
    190      * @param {Event} e The 'sorted' event.
    191      */
    192     handleSorted_: function(e) {
    193       var sm = this.list_.selectionModel;
    194       sm.adjustToReordering(e.sortPermutation);
    195 
    196       this.redraw();
    197       if (sm.leadIndex != -1)
    198         this.list_.scrollIndexIntoView(sm.leadIndex)
    199     },
    200 
    201     /**
    202      * Sort data by the given column.
    203      * @param {number} index The index of the column to sort by.
    204      */
    205     sort: function(i) {
    206       var cm = this.columnModel_;
    207       var sortStatus = this.list_.dataModel.sortStatus;
    208       if (sortStatus.field == cm.getId(i)) {
    209         var sortDirection = sortStatus.direction == 'desc' ? 'asc' : 'desc';
    210         this.list_.dataModel.sort(sortStatus.field, sortDirection);
    211       } else {
    212         this.list_.dataModel.sort(cm.getId(i), 'asc');
    213       }
    214     },
    215 
    216     /**
    217      * Called when an element in the table is focused. Marks the table as having
    218      * a focused element, and dispatches an event if it didn't have focus.
    219      * @param {Event} e The focus event.
    220      * @private
    221      */
    222     handleElementFocus_: function(e) {
    223       if (!this.hasElementFocus) {
    224         this.hasElementFocus = true;
    225         // Force styles based on hasElementFocus to take effect.
    226         this.list_.redraw();
    227       }
    228     },
    229 
    230     /**
    231      * Called when an element in the table is blurred. If focus moves outside
    232      * the table, marks the table as no longer having focus and dispatches an
    233      * event.
    234      * @param {Event} e The blur event.
    235      * @private
    236      */
    237     handleElementBlur_: function(e) {
    238       // When the blur event happens we do not know who is getting focus so we
    239       // delay this a bit until we know if the new focus node is outside the
    240       // table.
    241       var table = this;
    242       var list = this.list_;
    243       var doc = e.target.ownerDocument;
    244       window.setTimeout(function() {
    245         var activeElement = doc.activeElement;
    246         if (!table.contains(activeElement)) {
    247           table.hasElementFocus = false;
    248           // Force styles based on hasElementFocus to take effect.
    249           list.redraw();
    250         }
    251       });
    252     },
    253   };
    254 
    255   /**
    256    * Whether the table or one of its descendents has focus. This is necessary
    257    * because table contents can contain controls that can be focused, and for
    258    * some purposes (e.g., styling), the table can still be conceptually focused
    259    * at that point even though it doesn't actually have the page focus.
    260    */
    261   cr.defineProperty(Table, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);
    262 
    263   return {
    264     Table: Table
    265   };
    266 });
    267