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