Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / table.js
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 });