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