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 9cr.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 * Sets width of the column at the given index. 158 * 159 * @param {number} index The index of the column. 160 * @param {number} Column width. 161 */ 162 setColumnWidth: function(index, width) { 163 this.columnWidths_[index] = width; 164 }, 165 166 /** 167 * Initializes the element. 168 */ 169 decorate: function() { 170 this.list_ = this.ownerDocument.createElement('list'); 171 TableList.decorate(this.list_); 172 this.list_.selectionModel = new ListSelectionModel(this); 173 this.list_.table = this; 174 175 this.header_ = this.ownerDocument.createElement('div'); 176 TableHeader.decorate(this.header_); 177 this.header_.table = this; 178 179 this.classList.add('table'); 180 this.appendChild(this.header_); 181 this.appendChild(this.list_); 182 this.ownerDocument.defaultView.addEventListener( 183 'resize', this.header_.updateWidth.bind(this.header_)); 184 185 this.boundResize_ = this.resize.bind(this); 186 this.boundHandleSorted_ = this.handleSorted_.bind(this); 187 this.boundHandleChangeList_ = this.handleChangeList_.bind(this); 188 189 // The contained list should be focusable, not the table itself. 190 if (this.hasAttribute('tabindex')) { 191 this.list_.setAttribute('tabindex', this.getAttribute('tabindex')); 192 this.removeAttribute('tabindex'); 193 } 194 195 this.addEventListener('focus', this.handleElementFocus_, true); 196 this.addEventListener('blur', this.handleElementBlur_, true); 197 }, 198 199 /** 200 * Redraws the table. 201 */ 202 redraw: function(index) { 203 this.list_.redraw(); 204 this.header_.redraw(); 205 }, 206 207 startBatchUpdates: function() { 208 this.list_.startBatchUpdates(); 209 this.header_.startBatchUpdates(); 210 }, 211 212 endBatchUpdates: function() { 213 this.list_.endBatchUpdates(); 214 this.header_.endBatchUpdates(); 215 }, 216 217 /** 218 * Resize the table columns. 219 */ 220 resize: function() { 221 // We resize columns only instead of full redraw. 222 this.list_.resize(); 223 this.header_.resize(); 224 }, 225 226 /** 227 * Ensures that a given index is inside the viewport. 228 * @param {number} index The index of the item to scroll into view. 229 * @return {boolean} Whether any scrolling was needed. 230 */ 231 scrollIndexIntoView: function(i) { 232 this.list_.scrollIndexIntoView(i); 233 }, 234 235 /** 236 * Find the list item element at the given index. 237 * @param {number} index The index of the list item to get. 238 * @return {ListItem} The found list item or null if not found. 239 */ 240 getListItemByIndex: function(index) { 241 return this.list_.getListItemByIndex(index); 242 }, 243 244 /** 245 * This handles data model 'sorted' event. 246 * After sorting we need to redraw header 247 * @param {Event} e The 'sorted' event. 248 */ 249 handleSorted_: function(e) { 250 this.header_.redraw(); 251 }, 252 253 /** 254 * This handles data model 'change' and 'splice' events. 255 * Since they may change the visibility of scrollbar, table may need to 256 * re-calculation the width of column headers. 257 * @param {Event} e The 'change' or 'splice' event. 258 */ 259 handleChangeList_: function(e) { 260 webkitRequestAnimationFrame(this.header_.updateWidth.bind(this.header_)); 261 }, 262 263 /** 264 * Sort data by the given column. 265 * @param {number} index The index of the column to sort by. 266 */ 267 sort: function(i) { 268 var cm = this.columnModel_; 269 var sortStatus = this.list_.dataModel.sortStatus; 270 if (sortStatus.field == cm.getId(i)) { 271 var sortDirection = sortStatus.direction == 'desc' ? 'asc' : 'desc'; 272 this.list_.dataModel.sort(sortStatus.field, sortDirection); 273 } else { 274 this.list_.dataModel.sort(cm.getId(i), 'asc'); 275 } 276 }, 277 278 /** 279 * Called when an element in the table is focused. Marks the table as having 280 * a focused element, and dispatches an event if it didn't have focus. 281 * @param {Event} e The focus event. 282 * @private 283 */ 284 handleElementFocus_: function(e) { 285 if (!this.hasElementFocus) { 286 this.hasElementFocus = true; 287 // Force styles based on hasElementFocus to take effect. 288 this.list_.redraw(); 289 } 290 }, 291 292 /** 293 * Called when an element in the table is blurred. If focus moves outside 294 * the table, marks the table as no longer having focus and dispatches an 295 * event. 296 * @param {Event} e The blur event. 297 * @private 298 */ 299 handleElementBlur_: function(e) { 300 // When the blur event happens we do not know who is getting focus so we 301 // delay this a bit until we know if the new focus node is outside the 302 // table. 303 var table = this; 304 var list = this.list_; 305 var doc = e.target.ownerDocument; 306 window.setTimeout(function() { 307 var activeElement = doc.activeElement; 308 if (!table.contains(activeElement)) { 309 table.hasElementFocus = false; 310 // Force styles based on hasElementFocus to take effect. 311 list.redraw(); 312 } 313 }); 314 }, 315 }; 316 317 /** 318 * Whether the table or one of its descendents has focus. This is necessary 319 * because table contents can contain controls that can be focused, and for 320 * some purposes (e.g., styling), the table can still be conceptually focused 321 * at that point even though it doesn't actually have the page focus. 322 */ 323 cr.defineProperty(Table, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR); 324 325 return { 326 Table: Table 327 }; 328}); 329