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