1<!DOCTYPE html>
2<!--
3Copyright (c) 2014 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<link rel="import" href="/tracing/ui/base/dom_helpers.html">
9<link rel="import" href="/tracing/ui/base/utils.html">
10
11<!--
12@fileoverview A container that constructs a table-like container.
13-->
14<script>
15'use strict';
16
17tr.exportTo('tr.ui.b', function() {
18
19  var TableFormat = {};
20
21  TableFormat.SelectionMode = {
22    // Selection disabled.
23    // Default highlight: none.
24    NONE: 0,
25
26    // Row selection mode.
27    // Default highlight: dark row.
28    ROW: 1,
29
30    // Cell selection mode.
31    // Default highlight: dark cell and light row.
32    CELL: 2
33  };
34
35  TableFormat.HighlightStyle = {
36    // Highlight depends on the current selection mode.
37    DEFAULT: 0,
38
39    // No highlight.
40    NONE: 1,
41
42    // Light highlight.
43    LIGHT: 2,
44
45    // Dark highlight.
46    DARK: 3
47  };
48
49  return {
50    TableFormat: TableFormat
51  };
52});
53</script>
54
55<polymer-element name="tr-ui-b-table">
56  <template>
57    <style>
58      :host {
59        display: flex;
60        flex-direction: column;
61      }
62
63      table {
64        font-size: 12px;
65
66        flex: 1 1 auto;
67        align-self: stretch;
68        border-collapse: separate;
69        border-spacing: 0;
70        border-width: 0;
71        -webkit-user-select: initial;
72      }
73
74      tr > td {
75        padding: 2px 4px 2px 4px;
76        vertical-align: text-top;
77      }
78
79      tr:focus,
80      td:focus {
81        outline: 1px dotted rgba(0,0,0,0.1);
82        outline-offset: 0;
83      }
84
85      button.toggle-button {
86        height: 15px;
87        line-height: 60%;
88        vertical-align: middle;
89        width: 100%;
90      }
91
92      button > * {
93        height: 15px;
94        vertical-align: middle;
95      }
96
97      td.button-column {
98        width: 30px;
99      }
100
101      table > thead > tr > td.sensitive:hover {
102        background-color: #fcfcfc;
103      }
104
105      table > thead > tr > td {
106        font-weight: bold;
107        text-align: left;
108
109        background-color: #eee;
110        white-space: nowrap;
111        overflow: hidden;
112        text-overflow: ellipsis;
113
114        border-top: 1px solid #ffffff;
115        border-bottom: 1px solid #aaa;
116      }
117
118      table > tfoot {
119        background-color: #eee;
120        font-weight: bold;
121      }
122
123      /* Light row and cell highlight. */
124      table > tbody[row-highlight-style="light"] > tr[selected],
125      table > tbody[cell-highlight-style="light"] > tr > td[selected] {
126        background-color: rgb(213, 236, 229);  /* light turquoise */
127      }
128      table > tbody[row-highlight-style="light"] >
129          tr:not(.empty-row):not([selected]):hover,
130      table > tbody[cell-highlight-style="light"] >
131          tr:not(.empty-row):not([selected]) > td:hover {
132        background-color: #f6f6f6;  /* light grey */
133      }
134
135      /* Dark row and cell highlight. */
136      table > tbody[row-highlight-style="dark"] > tr[selected],
137      table > tbody[cell-highlight-style="dark"] > tr > td[selected] {
138        background-color: rgb(103, 199, 165);  /* turquoise */
139      }
140      table > tbody[row-highlight-style="dark"] >
141          tr:not(.empty-row):not([selected]):hover,
142      table > tbody[cell-highlight-style="dark"] >
143          tr:not(.empty-row):not([selected]) > td:hover {
144        background-color: #e6e6e6;  /* grey */
145      }
146      table > tbody[row-highlight-style="dark"] > tr:hover[selected],
147      table > tbody[cell-highlight-style="dark"] > tr[selected] > td:hover {
148        background-color: rgb(171, 217, 202);  /* semi-light turquoise */
149      }
150
151      table > tbody > tr.empty-row > td {
152        color: #666;
153        font-style: italic;
154        text-align: center;
155      }
156
157      table > tbody.has-footer > tr:last-child > td {
158        border-bottom: 1px solid #aaa;
159      }
160
161      table > tfoot > tr:first-child > td {
162        border-top: 1px solid #ffffff;
163      }
164
165      expand-button {
166        -webkit-user-select: none;
167        display: inline-block;
168        cursor: pointer;
169        font-size: 9px;
170        min-width: 8px;
171        max-width: 8px;
172      }
173
174      .button-expanded {
175        transform: rotate(90deg);
176      }
177    </style>
178    <table>
179      <thead id="head">
180      </thead>
181      <tbody id="body">
182      </tbody>
183      <tfoot id="foot">
184      </tfoot>
185    </table>
186  </template>
187  <script>
188  'use strict';
189  (function() {
190    var RIGHT_ARROW = String.fromCharCode(0x25b6);
191    var UNSORTED_ARROW = String.fromCharCode(0x25BF);
192    var ASCENDING_ARROW = String.fromCharCode(0x25B4);
193    var DESCENDING_ARROW = String.fromCharCode(0x25BE);
194    var BASIC_INDENTATION = 8;
195
196    var SelectionMode = tr.ui.b.TableFormat.SelectionMode;
197    var HighlightStyle = tr.ui.b.TableFormat.HighlightStyle;
198
199    Polymer({
200      created: function() {
201        this.selectionMode_ = SelectionMode.NONE;
202        this.rowHighlightStyle_ = HighlightStyle.DEFAULT;
203        this.cellHighlightStyle_ = HighlightStyle.DEFAULT;
204        this.selectedTableRowInfo_ = undefined;
205        this.selectedColumnIndex_ = undefined;
206
207        this.tableColumns_ = [];
208        this.tableRows_ = [];
209        this.tableRowsInfo_ = new WeakMap();
210        this.tableFooterRows_ = [];
211        this.tableFooterRowsInfo_ = new WeakMap();
212        this.sortColumnIndex_ = undefined;
213        this.sortDescending_ = false;
214        this.columnsWithExpandButtons_ = [];
215        this.headerCells_ = [];
216        this.showHeader_ = true;
217        this.emptyValue_ = undefined;
218        this.subRowsPropertyName_ = 'subRows';
219        this.customizeTableRowCallback_ = undefined;
220      },
221
222      ready: function() {
223        this.$.body.addEventListener(
224            'keydown', this.onKeyDown_.bind(this), true);
225      },
226
227      clear: function() {
228        this.selectionMode_ = SelectionMode.NONE;
229        this.rowHighlightStyle_ = HighlightStyle.DEFAULT;
230        this.cellHighlightStyle_ = HighlightStyle.DEFAULT;
231        this.selectedTableRowInfo_ = undefined;
232        this.selectedColumnIndex_ = undefined;
233
234        this.textContent = '';
235        this.tableColumns_ = [];
236        this.tableRows_ = [];
237        this.tableRowsInfo_ = new WeakMap();
238        this.tableFooterRows_ = [];
239        this.tableFooterRowsInfo_ = new WeakMap();
240        this.sortColumnIndex_ = undefined;
241        this.sortDescending_ = false;
242        this.columnsWithExpandButtons_ = [];
243        this.headerCells_ = [];
244        this.subRowsPropertyName_ = 'subRows';
245        this.defaultExpansionStateCallback_ = undefined;
246      },
247
248      get showHeader() {
249        return this.showHeader_;
250      },
251
252      set showHeader(showHeader) {
253        this.showHeader_ = showHeader;
254        this.scheduleRebuildHeaders_();
255      },
256
257      set subRowsPropertyName(name) {
258        this.subRowsPropertyName_ = name;
259      },
260
261      /**
262       * This callback will be called whenever a body row is built
263       * for a userRow that has subRows and does not have an explicit
264       * isExpanded field.
265       * The callback should return true if the row should be expanded,
266       * or false if the row should be collapsed.
267       * @param {function(userRow, parentUserRow): boolean} cb The callback.
268       */
269      set defaultExpansionStateCallback(cb) {
270        this.defaultExpansionStateCallback_ = cb;
271        this.scheduleRebuildBody_();
272      },
273
274      /**
275       * This callback will be called whenever a body row is built.
276       * The callback's return value is ignored.
277       * @param {function(userRow, trElement)} cb The callback.
278       */
279      set customizeTableRowCallback(cb) {
280        this.customizeTableRowCallback_ = cb;
281        this.scheduleRebuildBody_();
282      },
283
284      get emptyValue() {
285        return this.emptyValue_;
286      },
287
288      set emptyValue(emptyValue) {
289        var previousEmptyValue = this.emptyValue_;
290        this.emptyValue_ = emptyValue;
291        if (this.tableRows_.length === 0 && emptyValue !== previousEmptyValue)
292          this.scheduleRebuildBody_();
293      },
294
295      /**
296       * Data objects should have the following fields:
297       *   mandatory: title, value
298       *   optional: width {string}, cmp {function}, colSpan {number},
299       *             showExpandButtons {boolean}, textAlign {string}
300       *
301       * @param {Array} columns An array of data objects.
302       */
303      set tableColumns(columns) {
304        // Figure out the columns with expand buttons...
305        var columnsWithExpandButtons = [];
306        for (var i = 0; i < columns.length; i++) {
307          if (columns[i].showExpandButtons)
308            columnsWithExpandButtons.push(i);
309        }
310        if (columnsWithExpandButtons.length === 0) {
311          // First column if none have specified.
312          columnsWithExpandButtons = [0];
313        }
314
315        // Sanity check columns.
316        for (var i = 0; i < columns.length; i++) {
317          var colInfo = columns[i];
318          if (colInfo.width === undefined)
319            continue;
320
321          var hasExpandButton = columnsWithExpandButtons.indexOf(i) !== -1;
322
323          var w = colInfo.width;
324          if (w) {
325            if (/\d+px/.test(w)) {
326              continue;
327            } else if (/\d+%/.test(w)) {
328              if (hasExpandButton) {
329                throw new Error('Columns cannot be %-sized and host ' +
330                                ' an expand button');
331              }
332            } else {
333              throw new Error('Unrecognized width string');
334            }
335          }
336        }
337
338        // Commit the change.
339        this.tableColumns_ = columns;
340        this.headerCells_ = [];
341        this.columnsWithExpandButtons_ = columnsWithExpandButtons;
342        this.sortColumnIndex = undefined;
343        this.scheduleRebuildHeaders_();
344
345        // Blow away the table rows, too.
346        this.tableRows = this.tableRows_;
347      },
348
349      get tableColumns() {
350        return this.tableColumns_;
351      },
352
353      /**
354       * @param {Array} rows An array of 'row' objects with the following
355       * fields:
356       *   optional: subRows An array of objects that have the same 'row'
357       *                     structure. Set subRowsPropertyName to use an
358       *                     alternative field name.
359       */
360      set tableRows(rows) {
361        this.selectedTableRowInfo_ = undefined;
362        this.selectedColumnIndex_ = undefined;
363        this.maybeUpdateSelectedRow_();
364        this.tableRows_ = rows;
365        this.tableRowsInfo_ = new WeakMap();
366        this.scheduleRebuildBody_();
367      },
368
369      get tableRows() {
370        return this.tableRows_;
371      },
372
373      set footerRows(rows) {
374        this.tableFooterRows_ = rows;
375        this.tableFooterRowsInfo_ = new WeakMap();
376        this.scheduleRebuildFooter_();
377      },
378
379      get footerRows() {
380        return this.tableFooterRows_;
381      },
382
383      set sortColumnIndex(number) {
384        if (number === this.sortColumnIndex_)
385          return;
386
387        if (number === undefined) {
388          this.sortColumnIndex_ = undefined;
389          this.updateHeaderArrows_();
390          this.dispatchSortingChangedEvent_();
391          return;
392        }
393
394        if (this.tableColumns_.length <= number)
395          throw new Error('Column number ' + number + ' is out of bounds.');
396        if (!this.tableColumns_[number].cmp)
397          throw new Error('Column ' + number + ' does not have a comparator.');
398
399        this.sortColumnIndex_ = number;
400        this.updateHeaderArrows_();
401        this.scheduleRebuildBody_();
402        this.dispatchSortingChangedEvent_();
403      },
404
405      get sortColumnIndex() {
406        return this.sortColumnIndex_;
407      },
408
409      set sortDescending(value) {
410        var newValue = !!value;
411
412        if (newValue !== this.sortDescending_) {
413          this.sortDescending_ = newValue;
414          this.updateHeaderArrows_();
415          this.scheduleRebuildBody_();
416          this.dispatchSortingChangedEvent_();
417        }
418      },
419
420      get sortDescending() {
421        return this.sortDescending_;
422      },
423
424      updateHeaderArrows_: function() {
425        for (var i = 0; i < this.headerCells_.length; i++) {
426          if (!this.tableColumns_[i].cmp) {
427            this.headerCells_[i].sideContent = '';
428            continue;
429          }
430          if (i !== this.sortColumnIndex_) {
431            this.headerCells_[i].sideContent = UNSORTED_ARROW;
432            continue;
433          }
434          this.headerCells_[i].sideContent = this.sortDescending_ ?
435            DESCENDING_ARROW : ASCENDING_ARROW;
436        }
437      },
438
439      sortRows_: function(rows) {
440        rows.sort(function(rowA, rowB) {
441          if (this.sortDescending_)
442            return this.tableColumns_[this.sortColumnIndex_].cmp(
443                rowB.userRow, rowA.userRow);
444          return this.tableColumns_[this.sortColumnIndex_].cmp(
445                rowA.userRow, rowB.userRow);
446        }.bind(this));
447        // Sort expanded sub rows recursively.
448        for (var i = 0; i < rows.length; i++) {
449          if (this.getExpandedForUserRow_(rows[i]))
450            this.sortRows_(rows[i][this.subRowsPropertyName_]);
451        }
452      },
453
454      generateHeaderColumns_: function() {
455        this.headerCells_ = [];
456        this.$.head.textContent = '';
457        if (!this.showHeader_)
458          return;
459
460        var tr = this.appendNewElement_(this.$.head, 'tr');
461        for (var i = 0; i < this.tableColumns_.length; i++) {
462          var td = this.appendNewElement_(tr, 'td');
463
464          var headerCell = document.createElement('tr-ui-b-table-header-cell');
465
466          if (this.showHeader)
467            headerCell.cellTitle = this.tableColumns_[i].title;
468          else
469            headerCell.cellTitle = '';
470
471          // If the table can be sorted by this column, attach a tap callback
472          // to the column.
473          if (this.tableColumns_[i].cmp) {
474            td.classList.add('sensitive');
475            headerCell.tapCallback = this.createSortCallback_(i);
476            // Set arrow position, depending on the sortColumnIndex.
477            if (this.sortColumnIndex_ === i)
478              headerCell.sideContent = this.sortDescending_ ?
479                DESCENDING_ARROW : ASCENDING_ARROW;
480            else
481              headerCell.sideContent = UNSORTED_ARROW;
482          }
483
484          td.appendChild(headerCell);
485          this.headerCells_.push(headerCell);
486        }
487      },
488
489      applySizes_: function() {
490        if (this.tableRows_.length === 0 && !this.showHeader)
491          return;
492        var rowToRemoveSizing;
493        var rowToSize;
494        if (this.showHeader) {
495          rowToSize = this.$.head.children[0];
496          rowToRemoveSizing = this.$.body.children[0];
497        } else {
498          rowToSize = this.$.body.children[0];
499          rowToRemoveSizing = this.$.head.children[0];
500        }
501        for (var i = 0; i < this.tableColumns_.length; i++) {
502          if (rowToRemoveSizing && rowToRemoveSizing.children[i]) {
503            var tdToRemoveSizing = rowToRemoveSizing.children[i];
504            tdToRemoveSizing.style.minWidth = '';
505            tdToRemoveSizing.style.width = '';
506          }
507
508          // Apply sizing.
509          var td = rowToSize.children[i];
510
511          var delta;
512          if (this.columnsWithExpandButtons_.indexOf(i) !== -1) {
513            td.style.paddingLeft = BASIC_INDENTATION + 'px';
514            delta = BASIC_INDENTATION + 'px';
515          } else {
516            delta = undefined;
517          }
518
519          function calc(base, delta) {
520            if (delta)
521              return 'calc(' + base + ' - ' + delta + ')';
522            else
523              return base;
524          }
525
526          var w = this.tableColumns_[i].width;
527          if (w) {
528            if (/\d+px/.test(w)) {
529              td.style.minWidth = calc(w, delta);
530            } else if (/\d+%/.test(w)) {
531              td.style.width = w;
532            } else {
533              throw new Error('Unrecognized width string: ' + w);
534            }
535          }
536        }
537      },
538
539      createSortCallback_: function(columnNumber) {
540        return function() {
541          var previousIndex = this.sortColumnIndex;
542          this.sortColumnIndex = columnNumber;
543          if (previousIndex !== columnNumber)
544            this.sortDescending = false;
545          else
546            this.sortDescending = !this.sortDescending;
547        }.bind(this);
548      },
549
550      generateTableRowNodes_: function(tableSection, userRows, rowInfoMap,
551                                       indentation, lastAddedRow,
552                                       parentRowInfo) {
553        if (this.sortColumnIndex_ !== undefined &&
554            tableSection === this.$.body) {
555          userRows = userRows.slice(); // Don't mess with the input data.
556          userRows.sort(function(rowA, rowB) {
557            var c = this.tableColumns_[this.sortColumnIndex_].cmp(
558                  rowA, rowB);
559            if (this.sortDescending_)
560              c = -c;
561            return c;
562          }.bind(this));
563        }
564
565        for (var i = 0; i < userRows.length; i++) {
566          var userRow = userRows[i];
567          var rowInfo = this.getOrCreateRowInfoFor_(rowInfoMap, userRow,
568                                                    parentRowInfo);
569          var htmlNode = this.getHTMLNodeForRowInfo_(
570              tableSection, rowInfo, rowInfoMap, indentation);
571
572          if (lastAddedRow === undefined) {
573            // Put first into the table.
574            tableSection.insertBefore(htmlNode, tableSection.firstChild);
575          } else {
576            // This is shorthand for insertAfter(htmlNode, lastAdded).
577            var nextSiblingOfLastAdded = lastAddedRow.nextSibling;
578            tableSection.insertBefore(htmlNode, nextSiblingOfLastAdded);
579          }
580          this.updateTabIndexForTableRowNode_(htmlNode);
581
582          lastAddedRow = htmlNode;
583          if (!rowInfo.isExpanded)
584            continue;
585
586          // Append subrows now.
587          lastAddedRow = this.generateTableRowNodes_(
588              tableSection, userRow[this.subRowsPropertyName_], rowInfoMap,
589              indentation + 1, lastAddedRow, rowInfo);
590        }
591        return lastAddedRow;
592      },
593
594      getOrCreateRowInfoFor_: function(rowInfoMap, userRow, parentRowInfo) {
595        var rowInfo = undefined;
596
597        if (rowInfoMap.has(userRow)) {
598          rowInfo = rowInfoMap.get(userRow);
599        } else {
600          rowInfo = {
601            userRow: userRow,
602            htmlNode: undefined,
603            parentRowInfo: parentRowInfo
604          };
605          rowInfoMap.set(userRow, rowInfo);
606        }
607
608        // Recompute isExpanded in case defaultExpansionStateCallback_ has
609        // changed.
610        rowInfo.isExpanded = this.getExpandedForUserRow_(userRow);
611
612        return rowInfo;
613      },
614
615      customizeTableRow_: function(userRow, trElement) {
616        if (!this.customizeTableRowCallback_)
617          return;
618        this.customizeTableRowCallback_(userRow, trElement);
619      },
620
621      getHTMLNodeForRowInfo_: function(tableSection, rowInfo,
622                                       rowInfoMap, indentation) {
623        if (rowInfo.htmlNode) {
624          this.customizeTableRow_(rowInfo.userRow, rowInfo.htmlNode);
625          return rowInfo.htmlNode;
626        }
627
628        var INDENT_SPACE = indentation * 16;
629        var INDENT_SPACE_NO_BUTTON = indentation * 16 + BASIC_INDENTATION;
630        var trElement = this.ownerDocument.createElement('tr');
631        rowInfo.htmlNode = trElement;
632        rowInfo.indentation = indentation;
633        trElement.rowInfo = rowInfo;
634        this.customizeTableRow_(rowInfo.userRow, trElement);
635
636        for (var i = 0; i < this.tableColumns_.length;) {
637          var td = this.appendNewElement_(trElement, 'td');
638          td.columnIndex = i;
639
640          var column = this.tableColumns_[i];
641          var value = column.value(rowInfo.userRow);
642          var colSpan = column.colSpan ? column.colSpan : 1;
643          td.style.colSpan = colSpan;
644          if (column.textAlign) {
645            td.style.textAlign = column.textAlign;
646          }
647
648          if (this.doesColumnIndexSupportSelection(i))
649            td.classList.add('supports-selection');
650
651          if (this.columnsWithExpandButtons_.indexOf(i) != -1) {
652            if (rowInfo.userRow[this.subRowsPropertyName_] &&
653                rowInfo.userRow[this.subRowsPropertyName_].length > 0) {
654              td.style.paddingLeft = INDENT_SPACE + 'px';
655              var expandButton = this.appendNewElement_(td,
656                  'expand-button');
657              expandButton.textContent = RIGHT_ARROW;
658              if (rowInfo.isExpanded)
659                expandButton.classList.add('button-expanded');
660            } else {
661              td.style.paddingLeft = INDENT_SPACE_NO_BUTTON + 'px';
662            }
663          }
664
665          if (value !== undefined)
666            td.appendChild(tr.ui.b.asHTMLOrTextNode(value, this.ownerDocument));
667
668          i += colSpan;
669        }
670
671        var isSelectable = tableSection === this.$.body;
672        var isExpandable = rowInfo.userRow[this.subRowsPropertyName_] &&
673            rowInfo.userRow[this.subRowsPropertyName_].length;
674
675        if (isSelectable || isExpandable) {
676          trElement.addEventListener('click', function(e) {
677            e.stopPropagation();
678            if (e.target.tagName == 'EXPAND-BUTTON') {
679              this.setExpandedForUserRow_(
680                  tableSection, rowInfoMap,
681                  rowInfo.userRow, !rowInfo.isExpanded);
682              return;
683            }
684
685            function getTD(cur) {
686              if (cur === trElement)
687                throw new Error('woah');
688              if (cur.parentElement === trElement)
689                return cur;
690              return getTD(cur.parentElement);
691            }
692
693            // If the row/cell can be selected and it's not selected yet,
694            // select it.
695            if (isSelectable && this.selectionMode_ !== SelectionMode.NONE) {
696              var shouldSelect = false;
697              var columnIndex = getTD(e.target).columnIndex;
698              switch (this.selectionMode_) {
699                case SelectionMode.ROW:
700                  shouldSelect = this.selectedTableRowInfo_ !== rowInfo;
701                  break;
702
703                case SelectionMode.CELL:
704                  if (this.doesColumnIndexSupportSelection(columnIndex)) {
705                    shouldSelect = this.selectedTableRowInfo_ !== rowInfo ||
706                        this.selectedColumnIndex_ !== columnIndex;
707                  }
708                  break;
709
710                default:
711                  throw new Error('Invalid selection mode ' +
712                      this.selectionMode_);
713              }
714              if (shouldSelect) {
715                this.didTableRowInfoGetClicked_(rowInfo, columnIndex);
716                return;
717              }
718            }
719
720            // Otherwise, if the row is expandable, expand/collapse it.
721            if (isExpandable) {
722              this.setExpandedForUserRow_(tableSection, rowInfoMap,
723                  rowInfo.userRow, !rowInfo.isExpanded);
724            }
725          }.bind(this));
726        }
727
728        return rowInfo.htmlNode;
729      },
730
731      removeSubNodes_: function(tableSection, rowInfo, rowInfoMap) {
732        if (rowInfo.userRow[this.subRowsPropertyName_] === undefined)
733          return;
734        for (var i = 0;
735             i < rowInfo.userRow[this.subRowsPropertyName_].length; i++) {
736          var subRow = rowInfo.userRow[this.subRowsPropertyName_][i];
737          var subRowInfo = rowInfoMap.get(subRow);
738          if (!subRowInfo)
739            continue;
740
741          var subNode = subRowInfo.htmlNode;
742          if (subNode && subNode.parentNode === tableSection) {
743            tableSection.removeChild(subNode);
744            this.removeSubNodes_(tableSection, subRowInfo, rowInfoMap);
745          }
746        }
747      },
748
749      scheduleRebuildHeaders_: function() {
750        this.headerDirty_ = true;
751        this.scheduleRebuild_();
752      },
753
754      scheduleRebuildBody_: function() {
755        this.bodyDirty_ = true;
756        this.scheduleRebuild_();
757      },
758
759      scheduleRebuildFooter_: function() {
760        this.footerDirty_ = true;
761        this.scheduleRebuild_();
762      },
763
764      scheduleRebuild_: function() {
765        if (this.rebuildPending_)
766          return;
767        this.rebuildPending_ = true;
768        setTimeout(function() {
769          this.rebuildPending_ = false;
770          this.rebuild();
771        }.bind(this), 0);
772      },
773
774      rebuildIfNeeded_: function() {
775        this.rebuild();
776      },
777
778      rebuild: function() {
779        var wasBodyOrHeaderDirty = this.headerDirty_ || this.bodyDirty_;
780
781        if (this.headerDirty_) {
782          this.generateHeaderColumns_();
783          this.headerDirty_ = false;
784        }
785        if (this.bodyDirty_) {
786          this.$.body.textContent = '';
787          this.generateTableRowNodes_(
788              this.$.body,
789              this.tableRows_, this.tableRowsInfo_, 0,
790              undefined, undefined);
791          if (this.tableRows_.length === 0 && this.emptyValue_ !== undefined) {
792            var trElement = this.ownerDocument.createElement('tr');
793            this.$.body.appendChild(trElement);
794            trElement.classList.add('empty-row');
795            var td = this.ownerDocument.createElement('td');
796            trElement.appendChild(td);
797            td.colSpan = this.tableColumns_.length;
798            var emptyValue = this.emptyValue_;
799            td.appendChild(
800                tr.ui.b.asHTMLOrTextNode(emptyValue, this.ownerDocument));
801          }
802          this.bodyDirty_ = false;
803        }
804
805        if (wasBodyOrHeaderDirty)
806          this.applySizes_();
807
808        if (this.footerDirty_) {
809          this.$.foot.textContent = '';
810          this.generateTableRowNodes_(
811              this.$.foot,
812              this.tableFooterRows_, this.tableFooterRowsInfo_, 0,
813              undefined, undefined);
814          if (this.tableFooterRowsInfo_.length) {
815            this.$.body.classList.add('has-footer');
816          } else {
817            this.$.body.classList.remove('has-footer');
818          }
819          this.footerDirty_ = false;
820        }
821      },
822
823      appendNewElement_: function(parent, tagName) {
824        var element = parent.ownerDocument.createElement(tagName);
825        parent.appendChild(element);
826        return element;
827      },
828
829      getExpandedForTableRow: function(userRow) {
830        this.rebuildIfNeeded_();
831        var rowInfo = this.tableRowsInfo_.get(userRow);
832        if (rowInfo === undefined)
833          throw new Error('Row has not been seen, must expand its parents');
834        return rowInfo.isExpanded;
835      },
836
837      getExpandedForUserRow_: function(userRow) {
838        if (userRow[this.subRowsPropertyName_] === undefined)
839          return false;
840        if (userRow[this.subRowsPropertyName_].length === 0)
841          return false;
842        if (userRow.isExpanded)
843          return true;
844        if (userRow.isExpanded === false)
845          return false;
846        if (this.defaultExpansionStateCallback_ === undefined)
847          return false;
848
849        var parentUserRow = undefined;
850        var rowInfo = this.tableRowsInfo_.get(userRow);
851        if (rowInfo && rowInfo.parentRowInfo)
852          parentUserRow = rowInfo.parentRowInfo.userRow;
853
854        return this.defaultExpansionStateCallback_(
855            userRow, parentUserRow);
856      },
857
858      setExpandedForTableRow: function(userRow, expanded) {
859        this.rebuildIfNeeded_();
860        var rowInfo = this.tableRowsInfo_.get(userRow);
861        if (rowInfo === undefined)
862          throw new Error('Row has not been seen, must expand its parents');
863        return this.setExpandedForUserRow_(this.$.body, this.tableRowsInfo_,
864                                           userRow, expanded);
865      },
866
867      setExpandedForUserRow_: function(tableSection, rowInfoMap,
868                                       userRow, expanded) {
869        this.rebuildIfNeeded_();
870
871        var rowInfo = rowInfoMap.get(userRow);
872        if (rowInfo === undefined)
873          throw new Error('Row has not been seen, must expand its parents');
874
875        rowInfo.isExpanded = !!expanded;
876        // If no node, then nothing further needs doing.
877        if (rowInfo.htmlNode === undefined)
878          return;
879
880        // If its detached, then nothing needs doing.
881        if (rowInfo.htmlNode.parentElement !== tableSection)
882          return;
883
884        // Otherwise, rebuild.
885        var expandButton = rowInfo.htmlNode.querySelector('expand-button');
886        if (rowInfo.isExpanded) {
887          expandButton.classList.add('button-expanded');
888          var lastAddedRow = rowInfo.htmlNode;
889          if (rowInfo.userRow[this.subRowsPropertyName_]) {
890            this.generateTableRowNodes_(
891                tableSection,
892                rowInfo.userRow[this.subRowsPropertyName_], rowInfoMap,
893                rowInfo.indentation + 1,
894                lastAddedRow, rowInfo);
895          }
896        } else {
897          expandButton.classList.remove('button-expanded');
898          this.removeSubNodes_(tableSection, rowInfo, rowInfoMap);
899        }
900
901        this.maybeUpdateSelectedRow_();
902      },
903
904      get selectionMode() {
905        return this.selectionMode_;
906      },
907
908      set selectionMode(selectionMode) {
909        if (!tr.b.dictionaryContainsValue(SelectionMode, selectionMode))
910          throw new Error('Invalid selection mode ' + selectionMode);
911        this.rebuildIfNeeded_();
912        this.selectionMode_ = selectionMode;
913        this.didSelectionStateChange_();
914      },
915
916      get rowHighlightStyle() {
917        return this.rowHighlightStyle_;
918      },
919
920      set rowHighlightStyle(rowHighlightStyle) {
921        if (!tr.b.dictionaryContainsValue(HighlightStyle, rowHighlightStyle))
922          throw new Error('Invalid row highlight style ' + rowHighlightStyle);
923        this.rebuildIfNeeded_();
924        this.rowHighlightStyle_ = rowHighlightStyle;
925        this.didSelectionStateChange_();
926      },
927
928      get resolvedRowHighlightStyle() {
929        if (this.rowHighlightStyle_ !== HighlightStyle.DEFAULT)
930          return this.rowHighlightStyle_;
931        switch (this.selectionMode_) {
932          case SelectionMode.NONE:
933            return HighlightStyle.NONE;
934          case SelectionMode.ROW:
935            return HighlightStyle.DARK;
936          case SelectionMode.CELL:
937            return HighlightStyle.LIGHT;
938          default:
939            throw new Error('Invalid selection mode ' + selectionMode);
940        }
941      },
942
943      get cellHighlightStyle() {
944        return this.cellHighlightStyle_;
945      },
946
947      set cellHighlightStyle(cellHighlightStyle) {
948        if (!tr.b.dictionaryContainsValue(HighlightStyle, cellHighlightStyle))
949          throw new Error('Invalid cell highlight style ' + cellHighlightStyle);
950        this.rebuildIfNeeded_();
951        this.cellHighlightStyle_ = cellHighlightStyle;
952        this.didSelectionStateChange_();
953      },
954
955      get resolvedCellHighlightStyle() {
956        if (this.cellHighlightStyle_ !== HighlightStyle.DEFAULT)
957          return this.cellHighlightStyle_;
958        switch (this.selectionMode_) {
959          case SelectionMode.NONE:
960          case SelectionMode.ROW:
961            return HighlightStyle.NONE;
962          case SelectionMode.CELL:
963            return HighlightStyle.DARK;
964          default:
965            throw new Error('Invalid selection mode ' + selectionMode);
966        }
967      },
968
969      setHighlightStyle_: function(highlightAttribute, resolvedHighlightStyle) {
970        switch (resolvedHighlightStyle) {
971          case HighlightStyle.NONE:
972            this.$.body.removeAttribute(highlightAttribute);
973            break;
974          case HighlightStyle.LIGHT:
975            this.$.body.setAttribute(highlightAttribute, 'light');
976            break;
977          case HighlightStyle.DARK:
978            this.$.body.setAttribute(highlightAttribute, 'dark');
979            break;
980          default:
981            throw new Error('Invalid resolved highlight style ' +
982                resolvedHighlightStyle);
983        }
984      },
985
986      didSelectionStateChange_: function() {
987        this.setHighlightStyle_('row-highlight-style',
988            this.resolvedRowHighlightStyle);
989        this.setHighlightStyle_('cell-highlight-style',
990            this.resolvedCellHighlightStyle);
991
992        for (var i = 0; i < this.$.body.children.length; i++)
993          this.updateTabIndexForTableRowNode_(this.$.body.children[i]);
994        this.maybeUpdateSelectedRow_();
995      },
996
997      maybeUpdateSelectedRow_: function() {
998        if (this.selectedTableRowInfo_ === undefined)
999          return;
1000
1001        // Selection may be off.
1002        if (this.selectionMode_ === SelectionMode.NONE) {
1003          this.removeSelectedState_();
1004          this.selectedTableRowInfo_ = undefined;
1005          return;
1006        }
1007
1008        // selectedUserRow may not be visible
1009        function isVisible(rowInfo) {
1010          if (!rowInfo.htmlNode)
1011            return false;
1012          return !!rowInfo.htmlNode.parentElement;
1013        }
1014        if (isVisible(this.selectedTableRowInfo_)) {
1015          this.updateSelectedState_();
1016          return;
1017        }
1018
1019        this.removeSelectedState_();
1020        var curRowInfo = this.selectedTableRowInfo_;
1021        while (curRowInfo && !isVisible(curRowInfo))
1022          curRowInfo = curRowInfo.parentRowInfo;
1023
1024        this.selectedTableRowInfo_ = curRowInfo;
1025        if (this.selectedTableRowInfo_)
1026          this.updateSelectedState_();
1027      },
1028
1029      didTableRowInfoGetClicked_: function(rowInfo, columnIndex) {
1030        switch (this.selectionMode_) {
1031          case SelectionMode.NONE:
1032            return;
1033
1034          case SelectionMode.CELL:
1035            if (!this.doesColumnIndexSupportSelection(columnIndex))
1036              return;
1037            if (this.selectedColumnIndex !== columnIndex)
1038              this.selectedColumnIndex = columnIndex;
1039            // Fall through.
1040
1041          case SelectionMode.ROW:
1042            if (this.selectedTableRowInfo_ !== rowInfo)
1043              this.selectedTableRow = rowInfo.userRow;
1044        }
1045      },
1046
1047      get selectedTableRow() {
1048        if (!this.selectedTableRowInfo_)
1049          return undefined;
1050        return this.selectedTableRowInfo_.userRow;
1051      },
1052
1053      set selectedTableRow(userRow) {
1054        this.rebuildIfNeeded_();
1055        if (this.selectionMode_ === SelectionMode.NONE)
1056          throw new Error('Selection is off.');
1057
1058        var rowInfo;
1059        if (userRow === undefined) {
1060          rowInfo = undefined;
1061        } else {
1062          rowInfo = this.tableRowsInfo_.get(userRow);
1063          if (!rowInfo)
1064            throw new Error('Row has not been seen, must expand its parents.');
1065        }
1066
1067        var e = this.prepareToChangeSelection_();
1068        this.selectedTableRowInfo_ = rowInfo;
1069
1070        if (this.selectedTableRowInfo_ === undefined) {
1071          this.selectedColumnIndex_ = undefined;
1072          this.removeSelectedState_();
1073        } else {
1074          switch (this.selectionMode_) {
1075            case SelectionMode.ROW:
1076              this.selectedColumnIndex_ = undefined;
1077              break;
1078
1079            case SelectionMode.CELL:
1080              if (this.selectedColumnIndex_ === undefined) {
1081                var i = this.getFirstSelectableColumnIndex_();
1082                if (i == -1)
1083                  throw new Error('Cannot find a selectable column.');
1084                this.selectedColumnIndex_ = i;
1085              }
1086              break;
1087
1088            default:
1089              throw new Error('Invalid selection mode ' + this.selectionMode_);
1090          }
1091          this.updateSelectedState_();
1092        }
1093
1094        this.dispatchEvent(e);
1095      },
1096
1097      updateTabIndexForTableRowNode_: function(row) {
1098        if (this.selectionMode_ === SelectionMode.ROW)
1099          row.tabIndex = 0;
1100        else
1101          row.removeAttribute('tabIndex');
1102
1103        var enableCellTab = this.selectionMode_ === SelectionMode.CELL;
1104        for (var i = 0; i < this.tableColumns_.length; i++) {
1105          var cell = row.children[i];
1106          if (enableCellTab && this.doesColumnIndexSupportSelection(i))
1107            cell.tabIndex = 0;
1108          else
1109            cell.removeAttribute('tabIndex');
1110        }
1111      },
1112
1113      prepareToChangeSelection_: function() {
1114        var e = new tr.b.Event('selection-changed');
1115        var previousSelectedRowInfo = this.selectedTableRowInfo_;
1116        if (previousSelectedRowInfo)
1117          e.previousSelectedTableRow = previousSelectedRowInfo.userRow;
1118        else
1119          e.previousSelectedTableRow = undefined;
1120
1121        this.removeSelectedState_();
1122
1123        return e;
1124      },
1125
1126      removeSelectedState_: function() {
1127        this.setSelectedState_(false);
1128      },
1129
1130      updateSelectedState_: function() {
1131        this.setSelectedState_(true);
1132      },
1133
1134      setSelectedState_: function(select) {
1135        if (this.selectedTableRowInfo_ === undefined)
1136          return;
1137
1138        // Row selection.
1139        var rowNode = this.selectedTableRowInfo_.htmlNode;
1140        if (select)
1141          rowNode.setAttribute('selected', true);
1142        else
1143          rowNode.removeAttribute('selected');
1144
1145        // Cell selection (if applicable).
1146        var cellNode = rowNode.children[this.selectedColumnIndex_];
1147        if (!cellNode)
1148          return;
1149        if (select)
1150          cellNode.setAttribute('selected', true);
1151        else
1152          cellNode.removeAttribute('selected');
1153      },
1154
1155      doesColumnIndexSupportSelection: function(columnIndex) {
1156        var columnInfo = this.tableColumns_[columnIndex];
1157        var scs = columnInfo.supportsCellSelection;
1158        if (scs === false)
1159          return false;
1160        return true;
1161      },
1162
1163      getFirstSelectableColumnIndex_: function() {
1164        for (var i = 0; i < this.tableColumns_.length; i++) {
1165          if (this.doesColumnIndexSupportSelection(i))
1166            return i;
1167        }
1168        return -1;
1169      },
1170
1171      getSelectableNodeGivenTableRowNode_: function(htmlNode) {
1172        switch (this.selectionMode_) {
1173          case SelectionMode.ROW:
1174            return htmlNode;
1175
1176          case SelectionMode.CELL:
1177            return htmlNode.children[this.selectedColumnIndex_];
1178
1179          default:
1180            throw new Error('Invalid selection mode ' + this.selectionMode_);
1181        }
1182      },
1183
1184      get selectedColumnIndex() {
1185        if (this.selectionMode_ !== SelectionMode.CELL)
1186          return undefined;
1187        return this.selectedColumnIndex_;
1188      },
1189
1190      set selectedColumnIndex(selectedColumnIndex) {
1191        this.rebuildIfNeeded_();
1192        if (this.selectionMode_ === SelectionMode.NONE)
1193          throw new Error('Selection is off.');
1194        if (selectedColumnIndex < 0 ||
1195            selectedColumnIndex >= this.tableColumns_.length)
1196          throw new Error('Invalid index');
1197        if (!this.doesColumnIndexSupportSelection(selectedColumnIndex))
1198          throw new Error('Selection is not supported on this column');
1199
1200        var e = this.prepareToChangeSelection_();
1201        this.selectedColumnIndex_ = selectedColumnIndex;
1202        if (this.selectedColumnIndex_ === undefined)
1203          this.selectedTableRowInfo_ = undefined;
1204        this.updateSelectedState_();
1205
1206        this.dispatchEvent(e);
1207      },
1208
1209      onKeyDown_: function(e) {
1210        if (this.selectionMode_ === SelectionMode.NONE)
1211          return;
1212        if (this.selectedTableRowInfo_ === undefined)
1213          return;
1214
1215        var code_to_command_names = {
1216          13: 'ENTER',
1217          37: 'ARROW_LEFT',
1218          38: 'ARROW_UP',
1219          39: 'ARROW_RIGHT',
1220          40: 'ARROW_DOWN'
1221        };
1222        var cmdName = code_to_command_names[e.keyCode];
1223        if (cmdName === undefined)
1224          return;
1225
1226        e.stopPropagation();
1227        e.preventDefault();
1228        this.performKeyCommand_(cmdName);
1229      },
1230
1231      performKeyCommand_: function(cmdName) {
1232        this.rebuildIfNeeded_();
1233
1234        var rowInfo = this.selectedTableRowInfo_;
1235        var htmlNode = rowInfo.htmlNode;
1236        if (cmdName === 'ARROW_UP') {
1237          var prev = htmlNode.previousElementSibling;
1238          if (prev) {
1239            tr.ui.b.scrollIntoViewIfNeeded(prev);
1240            this.selectedTableRow = prev.rowInfo.userRow;
1241            this.focusSelected_();
1242            return;
1243          }
1244          return;
1245        }
1246
1247        if (cmdName === 'ARROW_DOWN') {
1248          var next = htmlNode.nextElementSibling;
1249          if (next) {
1250            tr.ui.b.scrollIntoViewIfNeeded(next);
1251            this.selectedTableRow = next.rowInfo.userRow;
1252            this.focusSelected_();
1253            return;
1254          }
1255          return;
1256        }
1257
1258        if (cmdName === 'ARROW_RIGHT') {
1259          switch (this.selectionMode_) {
1260            case SelectionMode.ROW:
1261              if (rowInfo.userRow[this.subRowsPropertyName_] === undefined)
1262              return;
1263              if (rowInfo.userRow[this.subRowsPropertyName_].length === 0)
1264                return;
1265
1266              if (!rowInfo.isExpanded)
1267                this.setExpandedForTableRow(rowInfo.userRow, true);
1268              this.selectedTableRow =
1269                rowInfo.userRow[this.subRowsPropertyName_][0];
1270              this.focusSelected_();
1271              return;
1272
1273            case SelectionMode.CELL:
1274              var newIndex = this.selectedColumnIndex_ + 1;
1275              if (newIndex >= this.tableColumns_.length)
1276                return;
1277              if (!this.doesColumnIndexSupportSelection(newIndex))
1278                return;
1279              this.selectedColumnIndex = newIndex;
1280              this.focusSelected_();
1281              return;
1282
1283            default:
1284              throw new Error('Invalid selection mode ' + this.selectionMode_);
1285          }
1286        }
1287
1288        if (cmdName === 'ARROW_LEFT') {
1289          switch (this.selectionMode_) {
1290            case SelectionMode.ROW:
1291              if (rowInfo.isExpanded) {
1292                this.setExpandedForTableRow(rowInfo.userRow, false);
1293                this.focusSelected_();
1294                return;
1295              }
1296
1297              // Not expanded. Select parent...
1298              var parentRowInfo = rowInfo.parentRowInfo;
1299              if (parentRowInfo) {
1300                this.selectedTableRow = parentRowInfo.userRow;
1301                this.focusSelected_();
1302                return;
1303              }
1304              return;
1305
1306            case SelectionMode.CELL:
1307              var newIndex = this.selectedColumnIndex_ - 1;
1308              if (newIndex < 0)
1309                return;
1310              if (!this.doesColumnIndexSupportSelection(newIndex))
1311                return;
1312              this.selectedColumnIndex = newIndex;
1313              this.focusSelected_();
1314              return;
1315
1316            default:
1317              throw new Error('Invalid selection mode ' + this.selectionMode_);
1318          }
1319        }
1320
1321        if (cmdName === 'ENTER') {
1322          if (rowInfo.userRow[this.subRowsPropertyName_] === undefined)
1323            return;
1324          if (rowInfo.userRow[this.subRowsPropertyName_].length === 0)
1325            return;
1326          this.setExpandedForTableRow(rowInfo.userRow, !rowInfo.isExpanded);
1327          this.focusSelected_();
1328          return;
1329        }
1330
1331        throw new Error('Unrecognized command ' + cmdName);
1332      },
1333
1334      focusSelected_: function() {
1335        if (!this.selectedTableRowInfo_)
1336          return;
1337        var node = this.getSelectableNodeGivenTableRowNode_(
1338            this.selectedTableRowInfo_.htmlNode);
1339        node.focus();
1340      },
1341
1342      dispatchSortingChangedEvent_: function() {
1343        var e = new tr.b.Event('sort-column-changed');
1344        e.sortColumnIndex = this.sortColumnIndex_;
1345        e.sortDescending = this.sortDescending_;
1346        this.dispatchEvent(e);
1347      }
1348    });
1349  })();
1350  </script>
1351</polymer-element>
1352<polymer-element name="tr-ui-b-table-header-cell" on-tap="onTap_">
1353  <template>
1354  <style>
1355    :host {
1356      -webkit-user-select: none;
1357      display: flex;
1358    }
1359
1360    span {
1361      flex: 0 1 auto;
1362    }
1363
1364    side-element {
1365      -webkit-user-select: none;
1366      flex: 1 0 auto;
1367      padding-left: 4px;
1368      vertical-align: top;
1369      font-size: 15px;
1370      font-family: sans-serif;
1371      display: inline;
1372      line-height: 85%;
1373    }
1374  </style>
1375
1376    <span id="title"></span><side-element id="side"></side-element>
1377  </template>
1378
1379  <script>
1380  'use strict';
1381
1382  Polymer({
1383    created: function() {
1384      this.tapCallback_ = undefined;
1385      this.cellTitle_ = '';
1386    },
1387
1388    set cellTitle(value) {
1389      this.cellTitle_ = value;
1390
1391      var titleNode = tr.ui.b.asHTMLOrTextNode(
1392          this.cellTitle_, this.ownerDocument);
1393
1394      this.$.title.innerText = '';
1395      this.$.title.appendChild(titleNode);
1396    },
1397
1398    get cellTitle() {
1399      return this.cellTitle_;
1400    },
1401
1402    clearSideContent: function() {
1403      this.$.side.textContent = '';
1404    },
1405
1406    set sideContent(content) {
1407      this.$.side.textContent = content;
1408    },
1409
1410    get sideContent() {
1411      return this.$.side.textContent;
1412    },
1413
1414    set tapCallback(callback) {
1415      this.style.cursor = 'pointer';
1416      this.tapCallback_ = callback;
1417    },
1418
1419    get tapCallback() {
1420      return this.tapCallback_;
1421    },
1422
1423    onTap_: function() {
1424      if (this.tapCallback_)
1425        this.tapCallback_();
1426    }
1427  });
1428</script>
1429</polymer-element>
1430