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.
6 * @fileoverview This implements a table control.
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;
18 * Creates a new table element.
19 * @param {Object=} opt_propertyBag Optional properties.
21 * @extends {HTMLDivElement}
23 var Table = cr.ui.define('div');
26 __proto__: HTMLDivElement.prototype,
28 columnModel_: new TableColumnModel([]),
31 * The table data model.
33 * @type {cr.ui.ArrayDataModel}
36 return this.list_.dataModel;
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_);
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_);
57 this.header_.redraw();
71 * The table column model.
73 * @type {cr.ui.table.TableColumnModel}
76 return this.columnModel_;
78 set columnModel(columnModel) {
79 if (this.columnModel_ != columnModel) {
80 if (this.columnModel_)
81 this.columnModel_.removeEventListener('resize', this.boundResize_);
82 this.columnModel_ = columnModel;
84 if (this.columnModel_)
85 this.columnModel_.addEventListener('resize', this.boundResize_);
86 this.list_.invalidate();
92 * The table selection model.
95 * {cr.ui.ListSelectionModel|cr.ui.table.ListSingleSelectionModel}
97 get selectionModel() {
98 return this.list_.selectionModel;
100 set selectionModel(selectionModel) {
101 if (this.list_.selectionModel != selectionModel) {
103 selectionModel.adjustLength(this.dataModel.length);
104 this.list_.selectionModel = selectionModel;
109 * The accessor to "autoExpands" property of the list.
114 return this.list_.autoExpands;
116 set autoExpands(autoExpands) {
117 this.list_.autoExpands = autoExpands;
121 return this.list_.fixedHeight;
123 set fixedHeight(fixedHeight) {
124 this.list_.fixedHeight = fixedHeight;
128 * Returns render function for row.
129 * @return {Function(*, cr.ui.Table): HTMLElement} Render function.
131 getRenderFunction: function() {
132 return this.list_.renderFunction_;
136 * Sets render function for row.
137 * @param {Function(*, cr.ui.Table): HTMLElement} Render function.
139 setRenderFunction: function(renderFunction) {
140 if (renderFunction === this.list_.renderFunction_)
143 this.list_.renderFunction_ = renderFunction;
144 cr.dispatchSimpleEvent(this, 'change');
148 * The header of the table.
150 * @type {cr.ui.table.TableColumnModel}
157 * Initializes the element.
159 decorate: function() {
160 this.header_ = this.ownerDocument.createElement('div');
161 this.list_ = this.ownerDocument.createElement('list');
163 this.appendChild(this.header_);
164 this.appendChild(this.list_);
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));
171 TableHeader.decorate(this.header_);
172 this.header_.table = this;
174 this.classList.add('table');
176 this.boundResize_ = this.resize.bind(this);
177 this.boundHandleSorted_ = this.handleSorted_.bind(this);
178 this.boundHandleChangeList_ = this.handleChangeList_.bind(this);
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');
186 this.addEventListener('focus', this.handleElementFocus_, true);
187 this.addEventListener('blur', this.handleElementBlur_, true);
193 redraw: function(index) {
195 this.header_.redraw();
198 startBatchUpdates: function() {
199 this.list_.startBatchUpdates();
200 this.header_.startBatchUpdates();
203 endBatchUpdates: function() {
204 this.list_.endBatchUpdates();
205 this.header_.endBatchUpdates();
209 * Resize the table columns.
212 // We resize columns only instead of full redraw.
214 this.header_.resize();
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.
222 scrollIndexIntoView: function(i) {
223 this.list_.scrollIndexIntoView(i);
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.
231 getListItemByIndex: function(index) {
232 return this.list_.getListItemByIndex(index);
236 * This handles data model 'sorted' event.
237 * After sorting we need to redraw header
238 * @param {Event} e The 'sorted' event.
240 handleSorted_: function(e) {
241 this.header_.redraw();
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.
250 handleChangeList_: function(e) {
251 requestAnimationFrame(this.header_.updateWidth.bind(this.header_));
255 * This handles list 'scroll' events. Scrolls the header accordingly.
256 * @param {Event} e Scroll event.
258 handleScroll_: function(e) {
259 this.header_.style.marginLeft = -this.list_.scrollLeft + 'px';
263 * Sort data by the given column.
264 * @param {number} i The index of the column to sort by.
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);
273 this.list_.dataModel.sort(cm.getId(i), cm.getDefaultOrder(i));
275 if (this.selectionModel.selectedIndex == -1)
276 this.list_.scrollTop = 0;
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.
285 handleElementFocus_: function(e) {
286 if (!this.hasElementFocus) {
287 this.hasElementFocus = true;
288 // Force styles based on hasElementFocus to take effect.
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
297 * @param {Event} e The blur event.
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
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.
318 * Adjust column width to fit its content.
319 * @param {number} index Index of the column to adjust width.
321 fitColumn: function(index) {
322 var list = this.list_;
323 var listHeight = list.clientHeight;
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);
331 var MAXIMUM_ROWS_TO_MEASURE = 1000;
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';
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);
356 list.appendChild(container);
357 var width = parseFloat(window.getComputedStyle(container).width);
358 list.removeChild(container);
359 cm.setWidth(index, width);
363 normalizeColumns: function() {
364 this.columnModel.normalizeWidths(this.clientWidth);
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.
374 cr.defineProperty(Table, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);