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
9cr.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