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