[Tables] - Sticky headers (#2071)

* first revision

* [Frontend] Styling for sticky table headers

Fixes #1481
- WIP convert mct-table layout to use flex;
- TODO: fix flex layout when a small number of rows;
- Rename CSS classes used as selectors by JS;

* remove header height from calculations since it is outside in its own table now

* [Frontend] Styling for sticky table headers

Fixes #1481
- Fixed flex layout when a small number of rows;
- Refined input padding and dropshadow for more compactness;

* fix tests and verify tables works properly in layout and large view

* add mct-scroll to header table to allow scrolling in sync with the rest of mct-table

* Various fixes and polishing

Fixes #2071
- Fix headers height issue;
- Move inline styles to classes;
- First round fix for horz overflow due to scrollbar problem;

* WIP horz overflow

Fixes #2071
- Commented out CSS-based scrollbar with approach in
anticipation of better JS solution;

* Horz overflow/scrollbar problem fixed

Fixes #2071
- Added calcTableWidthPx to allow sizing-table to subtract
width of scrollbar;

* Remove commented code

* add clear icon back into filter text boxes

* Polishing on sticky table headers filtering

Fixes #1481
Fixes #2071
- Now hides the magnify glass in table header filters when typing;
This commit is contained in:
Deep Tailor 2018-06-29 11:38:18 -07:00 committed by Pete Richards
parent 9d2c7a6de5
commit b8f278cabf
9 changed files with 146 additions and 113 deletions

View File

@ -111,7 +111,7 @@ $bubbleMaxW: 300px;
$reqSymbolW: 15px; $reqSymbolW: 15px;
$reqSymbolM: $interiorMargin * 2; $reqSymbolM: $interiorMargin * 2;
$reqSymbolFontSize: 0.75em; $reqSymbolFontSize: 0.75em;
$inputTextPTopBtm: 3px; $inputTextPTopBtm: 2px;
$inputTextPLeftRight: 5px; $inputTextPLeftRight: 5px;
$inputTextP: $inputTextPTopBtm $inputTextPLeftRight; $inputTextP: $inputTextPTopBtm $inputTextPLeftRight;
/*************** Wait Spinner Defaults */ /*************** Wait Spinner Defaults */

View File

@ -199,12 +199,18 @@ a.disabled {
} }
.vscroll { .vscroll {
overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
&.scroll-pad { &.scroll-pad {
padding-right: $interiorMargin; padding-right: $interiorMargin;
} }
} }
.vscroll--persist {
overflow-x: hidden;
overflow-y: scroll;
}
.slidable { .slidable {
cursor: move; // Fallback cursor: move; // Fallback
cursor: grab; cursor: grab;

View File

@ -334,7 +334,7 @@
color: $fg; color: $fg;
} }
@mixin nice-input($bg: $colorInputBg, $fg: $colorInputFg, $shdw: rgba(black, 0.6) 0 1px 3px) { @mixin nice-input($bg: $colorInputBg, $fg: $colorInputFg, $shdw: rgba(black, 0.5) 0 0px 2px) {
@include s-input($bg, $fg, $shdw); @include s-input($bg, $fg, $shdw);
border: none; border: none;
outline: none; outline: none;

View File

@ -26,6 +26,20 @@
.tabular-holder { .tabular-holder {
@include absPosDefault(); @include absPosDefault();
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
> * {
position: relative;
flex: 0 0 auto;
}
&.l-sticky-headers {
.l-tabular-body {
flex: 1 1 99%;
overflow-x: auto;
}
}
} }
.tabular, .tabular,
@ -41,19 +55,20 @@ table {
tbody tr, .tbody .tr { tbody tr, .tbody .tr {
width: 100%; width: 100%;
} }
thead, .thead {
border-bottom: 1px solid $colorTabHeaderBorder;
}
&:not(.fixed-header) tr th { thead tr th {
background-color: $colorTabHeaderBg; // Add some bg to the headers. Note that this is overwritten below
} // by .mct-table-headers-w when headers are wrapped by that container.
background-color: $colorTabHeaderBg;
}
tbody, .tbody { tbody, .tbody {
display: table-row-group; display: table-row-group;
tr, .tr {
border-top: 1px solid $colorTabBorder;
}
} }
tr, .tr { tr, .tr {
border-top: 1px solid $colorTabBorder;
display: table-row; display: table-row;
&:first-child .td { &:first-child .td {
border-top: none; border-top: none;
@ -118,42 +133,40 @@ table {
tbody, .tbody { tbody, .tbody {
top: $tabularHeaderH * 2; top: $tabularHeaderH * 2;
} }
input[type="text"], .l-filter {
input[type="search"] { input[type="text"],
box-sizing: border-box; input[type="search"] {
width: 100%; $p: 20px;
} transition: padding 200ms ease-in-out;
box-sizing: border-box;
padding-right: $p; // Fend off from icon
padding-left: $p; // Fend off from icon
width: 100%;
}
&.active {
// When user has typed something, hide the icon and collapse left padding
&:before {
opacity: 0;
}
input[type="text"],
input[type="search"] {
padding-left: $interiorMargin;
}
}
}
} }
&.fixed-header {
height: 100%;
thead, .thead,
tbody tr, .tbody .tr {
display: table;
table-layout: fixed;
}
thead, .thead {
width: calc(100% - 10px);
&:before {
content: "";
display: block;
z-index: 0;
position: absolute;
width: 100%;
height: $tabularHeaderH;
background-color: $colorTabHeaderBg;
}
}
tbody, .tbody {
@include absPosDefault(0);
top: $tabularHeaderH;
display: block;
overflow-y: scroll;
}
}
&.t-event-messages { &.t-event-messages {
td, .td { td, .td {
min-width: 150px; min-width: 150px;
} }
} }
} }
.mct-table-headers-w {
background: $colorTabHeaderBg;
overflow: hidden;
thead tr th {
background: none;
}
}

View File

@ -20,43 +20,48 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
.sizing-table { mct-table {
min-width: 100%; .mct-sizing-table {
z-index: -1; z-index: -1;
visibility: hidden; visibility: hidden;
position: absolute; position: absolute !important;
//Add some padding to allow for decorations such as limits indicator //Add some padding to allow for decorations such as limits indicator
td { td {
padding-right: 15px; padding-right: 15px;
padding-left: 10px; padding-left: 10px;
white-space: nowrap;
}
}
.mct-table {
table-layout: fixed;
thead {
display: block;
tr {
display: block;
white-space: nowrap; white-space: nowrap;
th { }
display: inline-block; }
.mct-table {
thead {
display: block;
tr {
display: block;
white-space: nowrap;
th {
display: inline-block;
box-sizing: border-box;
}
}
}
tbody {
tr {
position: absolute;
white-space: nowrap;
display: block;
}
td {
white-space: nowrap;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
display: inline-block;
} }
} }
} }
tbody {
tr { .l-control-bar {
position: absolute; margin-bottom: 3px;
white-space: nowrap;
display: block;
}
td {
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
display: inline-block;
}
} }
} }

View File

@ -5,22 +5,11 @@
Export Export
</a> </a>
</div> </div>
<div class="l-view-section scrolling" mct-resize="resize()"> <div class="mct-table-headers-w" mct-scroll-x="scroll.x">
<table class="sizing-table"> <table class="mct-table l-tabular-headers filterable"
<tbody> ng-style="{
<tr> 'max-width': totalWidth
<td ng-repeat="header in displayHeaders">{{header}}</td> }">
</tr>
<tr><td ng-repeat="header in displayHeaders" >
{{sizingRow[header].text}}
</td></tr>
</tbody>
</table>
<table class="filterable mct-table"
ng-style="{
height: totalHeight + 'px',
'max-width': totalWidth
}">
<thead> <thead>
<tr> <tr>
<th ng-repeat="header in displayHeaders" <th ng-repeat="header in displayHeaders"
@ -43,14 +32,38 @@
width: columnWidths[$index] + 'px', width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px', 'max-width': columnWidths[$index] + 'px',
}"> }">
<div class="holder l-filter flex-elem grows"> <div class="holder l-filter flex-elem grows"
<input type="search" ng-class="{active: filters[header]}">
ng-model="filters[header]"/> <input type="text"
<a class="clear-icon clear-input icon-x-in-circle" ng-class="{show: filters[header]}" ng-click="filters[header] = undefined"></a> ng-model="filters[header]"/>
<a class="clear-icon clear-input icon-x-in-circle"
ng-class="{show: filters[header]}"
ng-click="filters[header] = undefined"></a>
</div> </div>
</th> </th>
</tr> </tr>
</thead> </thead>
</table>
</div>
<table class="mct-sizing-table t-sizing-table"
ng-style="{
width: calcTableWidthPx
}">
<tbody>
<tr>
<td ng-repeat="header in displayHeaders">{{header}}</td>
</tr>
<tr><td ng-repeat="header in displayHeaders" >
{{sizingRow[header].text}}
</td></tr>
</tbody>
</table>
<div class="l-tabular-body t-scrolling vscroll--persist" mct-resize="resize()" mct-scroll-x="scroll.x">
<table class="mct-table"
ng-style="{
height: totalHeight + 'px',
'max-width': totalWidth
}">
<tbody> <tbody>
<tr ng-repeat-start="visibleRow in visibleRows track by $index" <tr ng-repeat-start="visibleRow in visibleRows track by $index"
ng-if="visibleRow.rowIndex === toiRowIndex" ng-if="visibleRow.rowIndex === toiRowIndex"

View File

@ -10,6 +10,6 @@
auto-scroll="autoScroll" auto-scroll="autoScroll"
default-sort="defaultSort" default-sort="defaultSort"
export-as="{{ exportAs }}" export-as="{{ exportAs }}"
class="tabular-holder has-control-bar"> class="tabular-holder l-sticky-headers has-control-bar">
</mct-table> </mct-table>
</div> </div>

View File

@ -23,10 +23,11 @@ define(
this.$window = $window; this.$window = $window;
this.maxDisplayRows = 100; this.maxDisplayRows = 100;
this.scrollable = this.element.find('.l-view-section.scrolling').first(); this.scrollable = this.element.find('.t-scrolling').first();
this.resultsHeader = this.element.find('.mct-table>thead').first(); this.resultsHeader = this.element.find('.mct-table>thead').first();
this.sizingTableBody = this.element.find('.sizing-table>tbody').first(); this.sizingTableBody = this.element.find('.t-sizing-table>tbody').first();
this.$scope.sizingRow = {}; this.$scope.sizingRow = {};
this.$scope.calcTableWidthPx = '100%';
this.timeApi = openmct.time; this.timeApi = openmct.time;
this.toiFormatter = undefined; this.toiFormatter = undefined;
this.formatService = formatService; this.formatService = formatService;
@ -286,14 +287,9 @@ define(
topScroll = target.scrollTop, topScroll = target.scrollTop,
firstVisible; firstVisible;
if (topScroll < this.$scope.headerHeight) { firstVisible = Math.floor(
firstVisible = 0; (topScroll) / this.$scope.rowHeight
} else { );
firstVisible = Math.floor(
(topScroll - this.$scope.headerHeight) /
this.$scope.rowHeight
);
}
return firstVisible; return firstVisible;
}; };
@ -309,7 +305,7 @@ define(
lastVisible; lastVisible;
lastVisible = Math.ceil( lastVisible = Math.ceil(
(bottomScroll - this.$scope.headerHeight) / (bottomScroll) /
this.$scope.rowHeight this.$scope.rowHeight
); );
return lastVisible; return lastVisible;
@ -360,8 +356,7 @@ define(
.map(function (row, i) { .map(function (row, i) {
return { return {
rowIndex: start + i, rowIndex: start + i,
offsetY: ((start + i) * self.$scope.rowHeight) + offsetY: ((start + i) * self.$scope.rowHeight),
self.$scope.headerHeight,
contents: row contents: row
}; };
}); });
@ -397,15 +392,13 @@ define(
* for individual rows. * for individual rows.
*/ */
MCTTableController.prototype.setElementSizes = function () { MCTTableController.prototype.setElementSizes = function () {
var thead = this.resultsHeader, var tbody = this.sizingTableBody,
tbody = this.sizingTableBody,
firstRow = tbody.find('tr'), firstRow = tbody.find('tr'),
column = firstRow.find('td'), column = firstRow.find('td'),
headerHeight = thead.prop('offsetHeight'),
rowHeight = firstRow.prop('offsetHeight'), rowHeight = firstRow.prop('offsetHeight'),
columnWidth, columnWidth,
tableWidth = 0, tableWidth = 0,
overallHeight = headerHeight + (rowHeight * overallHeight = (rowHeight *
(this.$scope.displayRows ? this.$scope.displayRows.length - 1 : 0)); (this.$scope.displayRows ? this.$scope.displayRows.length - 1 : 0));
this.$scope.columnWidths = []; this.$scope.columnWidths = [];
@ -416,10 +409,14 @@ define(
tableWidth += columnWidth; tableWidth += columnWidth;
column = column.next(); column = column.next();
} }
this.$scope.headerHeight = headerHeight;
this.$scope.rowHeight = rowHeight; this.$scope.rowHeight = rowHeight;
this.$scope.totalHeight = overallHeight; this.$scope.totalHeight = overallHeight;
var scrollW = this.scrollable[0].offsetWidth - this.scrollable[0].clientWidth;
if (scrollW && scrollW > 0) {
this.$scope.calcTableWidthPx = 'calc(100% - ' + scrollW + 'px)';
}
if (tableWidth > 0) { if (tableWidth > 0) {
this.$scope.totalWidth = tableWidth + 'px'; this.$scope.totalWidth = tableWidth + 'px';
} else { } else {
@ -761,7 +758,6 @@ define(
if (!visible) { if (!visible) {
var scrollTop = displayRowIndex * this.$scope.rowHeight + var scrollTop = displayRowIndex * this.$scope.rowHeight +
this.$scope.headerHeight -
(this.scrollable[0].offsetHeight / 2); (this.scrollable[0].offsetHeight / 2);
this.scrollable[0].scrollTop = scrollTop; this.scrollable[0].scrollTop = scrollTop;
this.setVisibleRows(); this.setVisibleRows();

View File

@ -29,7 +29,7 @@ define(
function ($, moment, MCTTableController) { function ($, moment, MCTTableController) {
var MOCK_ELEMENT_TEMPLATE = var MOCK_ELEMENT_TEMPLATE =
'<div><div class="l-view-section scrolling">' + '<div><div class="l-view-section t-scrolling">' +
'<table class="sizing-table"><tbody></tbody></table>' + '<table class="sizing-table"><tbody></tbody></table>' +
'<table class="mct-table"><thead></thead></table>' + '<table class="mct-table"><thead></thead></table>' +
'</div></div>'; '</div></div>';