Have an idea?

Visit Sawtooth Software Feedback to share your ideas on how we can improve our products.

Freeze Column Headers of a grid

Hello, I've looked everywhere and I've found answers on here that don't actually freeze the column headers of the grid question. I have all of the CSS/Javascript in the footer of the question to make it into a scrollable table, but I'm not sure what to manipulate to make it freeze the column headers, since I don't really know any programming languages. Here is the code that I currently have in the footer: the question name is "CurrentHealth"

<style>
#CurrentHealth_div > .question_body {
    height: 200px;
}
  
#CurrentHealth_div > .question_body > table {
    display: flex;
    flex-flow: column;
    height: 100%;
    width: 100%;
}
  
#CurrentHealth_div > .question_body > table > thead {
    /* head takes the height it requires,
    and it's not scaled when table is resized */
    flex: 0 0 auto;
    width: calc(100% - 0.9em);
}
  
#CurrentHealth_div > .question_body > table > tbody {
    /* body takes all the remaining available space */
    flex: 1 1 auto;
    display: block;
    height:200px;
    overflow-y: auto;
}
  
#CurrentHealth_div > .question_body > table > tbody > tr {
    width: 100%;
}
  
#CurrentHealth_div > .question_body > table > thead,
#CurrentHealth_div > .question_body > table > tbody > tr {
    display: table;
    table-layout: fixed;
}
</style>
<script>
$(document).ready(function(){
    $('#CurrentHealth_div > .question_body > table').prepend('<thead><tr></tr><tr></tr></thead>');
      
    $('#CurrentHealth_div > .question_body > table > tbody > tr:first-child > td').each(function(){
        var tdHtml = $(this).html();
        $('#CurrentHealth_div > .question_body > table > thead > tr:first-child').append('<thead>' + tdHtml + '</th>');
    });
    $('#CurrentHealth_div > .question_body > table > tbody > tr:first-child').remove();
     
    $('#CurrentHealth_div > .question_body > table > tbody > tr:first-child > td').each(function(){
        var tdHtml = $(this).html();
        $('#CurrentHealth_div > .question_body > table > thead > tr:last-child').append('<thead>' + tdHtml + '</thead>');
    });
    $('#CurrentHealth_div > .question_body > table > tbody > tr:first-child').remove();
})
</script>
asked Aug 27, 2020 by Corey
I believe the main issue lies with your two "append" lines.  The instances of "<thead>" and "</thead>" there ought to be "<th>" and "</th>," respectively.  Give that a shot and lets see where we end up.
Hi Zachary, thanks for the quick response. After making those changes, the table still looks the same, the column headers scroll with the whole table instead of being frozen.
I noticed that you tagged this question with ssi-web-7.  Is that the version you're running?  The code you've posted above uses jQuery, but SSI Web v7 did not come with jQuery installed.  Have you manually added jQuery to your survey?
Hi Zachary, I was not aware that I needed to do so. After some tinkering, I was able to add a 1-row table to the header2 section, and the rest of the code for the working scrolling box below it, but now I have an issue where the top table isn't scaling to the width of the question columns. Any ideas there? Here is the code:

PUT IN HEADER 2:

<div class="table-container">
    <table>
        <thead>
            <tr>
        <th>&nbsp;</th>
        <th>&nbsp;</th>
        <th>&nbsp;</th>
                <th>Poor</th>
                <th>Fair</th>
                <th>Good</th>
                <th>Very Good</th>
        <th>Excellent</th>
            </tr>
        </thead>
 </table>
</div>

<style>
.table-container {
    height: 2em;
}
table {
    display: flex;
    flex-flow: column;
    height: 100%;
    width: 100%;
}
table thead {
    /* head takes the height it requires,
    and it's not scaled when table is resized */
    flex: 0 0 auto;
    width: calc(100% - 0.9em);
}

table thead{
    display: table;
    table-layout: fixed;
}
</style>

____________________________________________________________________________

PUT IN FOOTER:

<style>
#CurrentHealth_div > .question_body {
    height: 200px;
}
  
#CurrentHealth_div > .question_body > table {
    display: flex;
    flex-flow: column;
    height: 100%;
    width: 100%;
}
  
#CurrentHealth_div > .question_body > table > thead {
    /* head takes the height it requires,
    and it's not scaled when table is resized */
    flex: 0 0 auto;
    width: calc(100% - 0.9em);
}
  
#CurrentHealth_div > .question_body > table > tbody {
    /* body takes all the remaining available space */
    flex: 1 1 auto;
    display: block;
    height:200px;
    overflow-y: auto;
}
  
#CurrentHealth_div > .question_body > table > tbody > tr {
    width: 100%;
}
  
#CurrentHealth_div > .question_body > table > thead,
#CurrentHealth_div > .question_body > table > tbody > tr {
    display: table;
    table-layout: fixed;
}
</style>
And here is the additional script in the footer:

<script>
$(document).ready(function(){
    $('#CurrentHealth_div > .question_body > table').append('<thead></thead>');
    $('#CurrentHealth_div > .question_body > table').append('<tr></tr><tr></tr>');
      
    $('#CurrentHealth_div > .question_body > table > tbody > tr:first-child > td').each(function(){
        var tdHtml = $(this).html();
        $('#CurrentHealth_div > .question_body > table > thead > tr:first-child').append('<thead>' + tdHtml + '</th>');
    });
    $('#CurrentHealth_div > .question_body > table > tbody > tr:first-child').remove();
     
    $('#CurrentHealth_div > .question_body > table > tbody > tr:first-child > td').each(function(){
        var tdHtml = $(this).html();
        $('#CurrentHealth_div > .question_body > table > thead > tr:last-child').append('<thead>' + tdHtml + '</thead>');
    });
    $('#CurrentHealth_div > .question_body > table > tbody > tr:first-child').remove();
})
</script>
Before we go down that route, perhaps we could just rewrite your jQuery script in plain JavaScript.  Try your original solution but with this JS instead:

<script>
var innerTable = document.querySelector('#[% QuestionName() %]_div .inner_table');
var theadHtml = '<thead><tr>';
var columnLabelTds = document.querySelectorAll('#[% QuestionName() %]_div .inner_table > tbody > tr:first-child > td');
for (var i = 0; i < columnLabelTds.length; i++) {
    theadHtml += '<th>' + columnLabelTds[i].innerHTML + '</th>';
}
theadHtml += '</tr></thead>';
innerTable.innerHTML = theadHtml + innerTable.innerHTML;
document.querySelector('#[% QuestionName() %]_div .inner_table > tbody > tr:first-child').style.display = 'none';
</script>
Zachary, you are a lifesaver. Those are the frozen column labels I was looking for! Now all that's left is they aren't quite lined up with the select buttons in the grid, how would I go about adjusting that in the code?
It looks like v7 has some weird code that affects thead elements.  Let's override it by adding this to our CSS:

thead {
    width: 100% !important;
}
Hi Zachary, it looks like pasting that in the CSS section of the code didn't do much of anything to the column headers lining up properly with the radio buttons.
I placed it immediately before your "</style>" and it worked for me.
Unfortunately still not lining up, is there any way you are open to connecting through a method where I can send a picture of the issue? Or share my screen in real time as another option.
A picture usually is fairly limited as far as debugging CSS.  If you upload your survey somewhere, I could see the misbehaving table in my browser.  Alternatively, if you share your .ssi with support@sawtoothsoftware.com, I can take a look.

Either way, you could remove irrelevant or sensitive information before sharing.
Hi Zachary, I have uploaded the survey with just the grid question at www.c3research.com/surveys/frozengridheadertest

Thanks again for all your help

1 Answer

0 votes
Ah, I suspect you have set the "Width of Labels Column" setting and that is throwing off the widths in the header row.  Try this adjustment to the JavaScript from earlier:

var innerTable = document.querySelector('#[% QuestionName() %]_div .inner_table');
var theadHtml = '<thead><tr>';
var columnLabelTds = document.querySelectorAll('#[% QuestionName() %]_div .inner_table > tbody > tr:first-child > td');
for (var i = 0; i < columnLabelTds.length; i++) {
    var width = columnLabelTds[i].getAttribute('width');
    theadHtml += '<th width="' + width + '">' + columnLabelTds[i].innerHTML + '</th>';
}
theadHtml += '</tr></thead>';
innerTable.innerHTML = theadHtml + innerTable.innerHTML;
document.querySelector('#[% QuestionName() %]_div .inner_table > tbody > tr:first-child').style.display = 'none';
answered Aug 28, 2020 by Zachary Platinum Sawtooth Software, Inc. (206,100 points)
Hi Zachary, that's a wonderful solution and one that scales with the "Width of Labels Column" setting as well as different sized column headers, but they still seem to be justified a bit to the right of their respective columns. I have reuploaded at the same link as before to see
Does appending this line to the end of the JavaScript help?

document.querySelector('#[% QuestionName() %]_div .inner_table > thead').style.width = document.querySelector('#[% QuestionName() %]_div .inner_table > tbody > tr:last-child').offsetWidth + 'px';
Hi Zachary, I originally thought that adding + '" to the end of that script did the trick, but it turns out my tinkering lined up the width, but removed the unscrollable header and added it back to the table. Unfortunately, your code didn't change the width to line up yet.
After much adjusting of the code, here is the CSS and Javascript that you can paste in a grid question footer to have a scrolling grid with a frozen column header -- thank you Zachary.

<style>
#QuestionName_div > .question_body {
    height: 300px;
}
  
#QuestionName_div > .question_body > table {
    display: flex;
    flex-flow: column;
    height: 100%;
    width: 100%;
}
  
#QuestionName_div > .question_body > table > thead {
    /* head takes the height it requires,
    and it's not scaled when table is resized */
    flex: 0 1 auto;
    width: calc(100% - 0.9em);
    text-align: center;
    
}
  
#QuestionName_div > .question_body > table > tbody {
    /* body takes all the remaining available space */
    flex: 1 1 auto;
    display: block;
    height:200px;
    overflow-y: auto;
}
  
#QuestionName_div > .question_body > table > tbody > tr {
    width: 100%;
}
  
#QuestionName_div > .question_body > table > thead,
#QuestionName_div > .question_body > table > tbody > tr {
    display: table;
    table-layout: fixed;
}
thead {
    width: calc(100% - 1.5em) !important;
}
</style>


Change all "#QuestionName" in the code above to the question name in your survey, and paste the Javascript below without changing anything:

<script>
var innerTable = document.querySelector('#[% QuestionName() %]_div .inner_table'); var theadHtml = '<thead><tr>'; var columnLabelTds = document.querySelectorAll('#[% QuestionName() %]_div .inner_table > tbody > tr:first-child > td'); for (var i = 0; i < columnLabelTds.length; i++) {
    var width = columnLabelTds[i].getAttribute('width');
    theadHtml += '<th width="' + width + '">' + columnLabelTds[i].innerHTML + '</th>'; } theadHtml += '</tr></thead>'; innerTable.innerHTML = theadHtml + innerTable.innerHTML; document.querySelector('#[% QuestionName() %]_div .inner_table > tbody > tr:first-child').style.display = 'none';
document.querySelector('#[% QuestionName() %]_div .inner_table > thead').style.width = document.querySelector('#[% QuestionName() %]_div .inner_table > tbody > tr:last-child').offsetWidth + '100%';  
</script>
...