Update To 11.40.268.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.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} renderFunction Render
138      *     function.
139      */
140     setRenderFunction: function(renderFunction) {
141       if (renderFunction === this.list_.renderFunction_)
142         return;
143
144       this.list_.renderFunction_ = renderFunction;
145       cr.dispatchSimpleEvent(this, 'change');
146     },
147
148     /**
149      * The header of the table.
150      *
151      * @type {cr.ui.table.TableColumnModel}
152      */
153     get header() {
154       return this.header_;
155     },
156
157     /**
158      * Initializes the element.
159      */
160     decorate: function() {
161       this.header_ = this.ownerDocument.createElement('div');
162       this.list_ = this.ownerDocument.createElement('list');
163
164       this.appendChild(this.header_);
165       this.appendChild(this.list_);
166
167       TableList.decorate(this.list_);
168       this.list_.selectionModel = new ListSelectionModel();
169       this.list_.table = this;
170       this.list_.addEventListener('scroll', this.handleScroll_.bind(this));
171
172       TableHeader.decorate(this.header_);
173       this.header_.table = this;
174
175       this.classList.add('table');
176
177       this.boundResize_ = this.resize.bind(this);
178       this.boundHandleSorted_ = this.handleSorted_.bind(this);
179       this.boundHandleChangeList_ = this.handleChangeList_.bind(this);
180
181       // The contained list should be focusable, not the table itself.
182       if (this.hasAttribute('tabindex')) {
183         this.list_.setAttribute('tabindex', this.getAttribute('tabindex'));
184         this.removeAttribute('tabindex');
185       }
186
187       this.addEventListener('focus', this.handleElementFocus_, true);
188       this.addEventListener('blur', this.handleElementBlur_, true);
189     },
190
191     /**
192      * Redraws the table.
193      */
194     redraw: function() {
195       this.list_.redraw();
196       this.header_.redraw();
197     },
198
199     startBatchUpdates: function() {
200       this.list_.startBatchUpdates();
201       this.header_.startBatchUpdates();
202     },
203
204     endBatchUpdates: function() {
205       this.list_.endBatchUpdates();
206       this.header_.endBatchUpdates();
207     },
208
209     /**
210      * Resize the table columns.
211      */
212     resize: function() {
213       // We resize columns only instead of full redraw.
214       this.list_.resize();
215       this.header_.resize();
216     },
217
218     /**
219      * Ensures that a given index is inside the viewport.
220      * @param {number} i The index of the item to scroll into view.
221      * @return {boolean} Whether any scrolling was needed.
222      */
223     scrollIndexIntoView: function(i) {
224       this.list_.scrollIndexIntoView(i);
225     },
226
227     /**
228      * Find the list item element at the given index.
229      * @param {number} index The index of the list item to get.
230      * @return {cr.ui.ListItem} The found list item or null if not found.
231      */
232     getListItemByIndex: function(index) {
233       return this.list_.getListItemByIndex(index);
234     },
235
236     /**
237      * This handles data model 'sorted' event.
238      * After sorting we need to redraw header
239      * @param {Event} e The 'sorted' event.
240      */
241     handleSorted_: function(e) {
242       this.header_.redraw();
243     },
244
245     /**
246      * This handles data model 'change' and 'splice' events.
247      * Since they may change the visibility of scrollbar, table may need to
248      * re-calculation the width of column headers.
249      * @param {Event} e The 'change' or 'splice' event.
250      */
251     handleChangeList_: function(e) {
252       requestAnimationFrame(this.header_.updateWidth.bind(this.header_));
253     },
254
255     /**
256      * This handles list 'scroll' events. Scrolls the header accordingly.
257      * @param {Event} e Scroll event.
258      */
259     handleScroll_: function(e) {
260       this.header_.style.marginLeft = -this.list_.scrollLeft + 'px';
261     },
262
263     /**
264      * Sort data by the given column.
265      * @param {number} i 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), cm.getDefaultOrder(i));
275       }
276       if (this.selectionModel.selectedIndex == -1)
277         this.list_.scrollTop = 0;
278     },
279
280     /**
281      * Called when an element in the table is focused. Marks the table as having
282      * a focused element, and dispatches an event if it didn't have focus.
283      * @param {Event} e The focus event.
284      * @private
285      */
286     handleElementFocus_: function(e) {
287       if (!this.hasElementFocus) {
288         this.hasElementFocus = true;
289         // Force styles based on hasElementFocus to take effect.
290         this.list_.redraw();
291       }
292     },
293
294     /**
295      * Called when an element in the table is blurred. If focus moves outside
296      * the table, marks the table as no longer having focus and dispatches an
297      * event.
298      * @param {Event} e The blur event.
299      * @private
300      */
301     handleElementBlur_: function(e) {
302       // When the blur event happens we do not know who is getting focus so we
303       // delay this a bit until we know if the new focus node is outside the
304       // table.
305       var table = this;
306       var list = this.list_;
307       var doc = e.target.ownerDocument;
308       window.setTimeout(function() {
309         var activeElement = doc.activeElement;
310         if (!table.contains(activeElement)) {
311           table.hasElementFocus = false;
312           // Force styles based on hasElementFocus to take effect.
313           list.redraw();
314         }
315       });
316     },
317
318     /**
319      * Adjust column width to fit its content.
320      * @param {number} index Index of the column to adjust width.
321      */
322     fitColumn: function(index) {
323       var list = this.list_;
324       var listHeight = list.clientHeight;
325
326       var cm = this.columnModel_;
327       var dm = this.dataModel;
328       var columnId = cm.getId(index);
329       var doc = this.ownerDocument;
330       var render = cm.getRenderFunction(index);
331       var table = this;
332       var MAXIMUM_ROWS_TO_MEASURE = 1000;
333
334       // Create a temporaty list item, put all cells into it and measure its
335       // width. Then remove the item. It fits "list > *" CSS rules.
336       var container = doc.createElement('li');
337       container.style.display = 'inline-block';
338       container.style.textAlign = 'start';
339       // The container will have width of the longest cell.
340       container.style.webkitBoxOrient = 'vertical';
341
342       // Ensure all needed data available.
343       dm.prepareSort(columnId, function() {
344         // Select at most MAXIMUM_ROWS_TO_MEASURE items around visible area.
345         var items = list.getItemsInViewPort(list.scrollTop, listHeight);
346         var firstIndex = Math.floor(Math.max(0,
347             (items.last + items.first - MAXIMUM_ROWS_TO_MEASURE) / 2));
348         var lastIndex = Math.min(dm.length,
349                                  firstIndex + MAXIMUM_ROWS_TO_MEASURE);
350         for (var i = firstIndex; i < lastIndex; i++) {
351           var item = dm.item(i);
352           var div = doc.createElement('div');
353           div.className = 'table-row-cell';
354           div.appendChild(render(item, columnId, table));
355           container.appendChild(div);
356         }
357         list.appendChild(container);
358         var width = parseFloat(window.getComputedStyle(container).width);
359         list.removeChild(container);
360         cm.setWidth(index, width);
361       });
362     },
363
364     normalizeColumns: function() {
365       this.columnModel.normalizeWidths(this.clientWidth);
366     }
367   };
368
369   /**
370    * Whether the table or one of its descendents has focus. This is necessary
371    * because table contents can contain controls that can be focused, and for
372    * some purposes (e.g., styling), the table can still be conceptually focused
373    * at that point even though it doesn't actually have the page focus.
374    */
375   cr.defineProperty(Table, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);
376
377   return {
378     Table: Table
379   };
380 });