Search code examples
razorcss-grid

Building CSS Grid with N columns, 2 rows, max height of container


New to Grid in CSS and, of course, starting with a more convoluted need.

Think an airport Arrivals/Departures display. No keyboard, no mouse, no human interaction. This callout app used to be just one big long scrolling list. I am turning it into a little more organized layout. Depending on what screen it finally shows up on it may have room for 2 columns, 3 columns maybe even 5 columns - where each column is the same width and there is a minimum width.

1st row: header / full width / 3 lines of text
2nd row: should be equal to the remaining height, full width and is a container
In the container:

  • 2 rows (title row, contents row)
  • N columns (where column has min-width) --> also a view panel for text in a marquee (vert scrolling via js)
@model wsGT4.Models.DictionaryResultSet<string, List<wsGT4.Models.Callout>>
@{
    ViewBag.Title = "Callout";
    Layout = "~/Views/Shared/_EmptyLayout.cshtml";
}


<section class="header">
    <H1>TOMORROW'S CALLOUTS WILL DISPLAY STARTING AT 5PM</H1>
    <H2>BRING DOCS TO CALLOUTS</H2>
    @if (Model.Success == false)
    {
        foreach (var msg in Model.Messages)
        {
            <h2>@msg</h2>
        }
    }
    else
    {
        if (DateTime.Now.AddHours(Model.TimezoneOffset).Hour >= 17)
        {
            <h2>CALLOUTS FOR <span style="color:red; background-color: yellow;">TOMORROW</span> - @DateTime.Now.AddDays(1).ToString("MMM dd") <span style="font-size:.6em">(@DateTime.Now.AddHours(Model.TimezoneOffset).ToString("HH:mm"))</span></h2>
        }
        else
        {
            <h2>CALLOUTS FOR TODAY - @DateTime.Now.ToString("MMM dd") <span style="font-size:.6em">(@DateTime.Now.AddHours(Model.TimezoneOffset).ToString("HH:mm"))</span></h2>
        }
    }
</section>
<section class="container">
    @foreach (var kvp in Model.ResultList.OrderBy(a => a.Key))
    {
        <div class="timeDisplay">
            &gt;&gt;&gt; @kvp.Key
        </div>
        <div class="viewPort">
            <div class="textList">
                @foreach (var subject in kvp.Value.OrderBy(a => a.LastName).ThenBy(a => a.SubjectId))
                {
                    <span>@subject.LastName</span>
                    <span>#@subject.SubjectId</span>
                    <span>@subject.EventTitle</span>
                }
            </div>
        </div>
    }
</section>

CSS (which I am royally screwing up)

.header {
    padding: 5px;
    border: groove;
    border-bottom-color: black;
    border-width: 2px;

    /* Grid styles */
    display: grid;
    align-items: center;
    /*grid-template-columns: 1fr;*/
    grid-template-rows: repeat(3, 1fr);
}
.container {
    max-height: 100vh;

    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-rows: 30px auto-fill;
    grid-gap: 5px;
}
.timeDisplay{
    max-height: 30px;
}
.viewPort {
    height: 100%;
    overflow: hidden;
}
.textList {
    height: 100%;
    font-size: 125%;
    display: grid;
    grid-template-columns: 3fr 2fr 5fr;
}

Since the model collection coming back can have an unknown number of time/list sets I am trying to let automation do some of the work. The idea being that if I have more columns than what can fit then they are either (A) not displayed or (B) wrap below the bottom of the screen, effectively being hidden.


Solution

  • So I changed my approach a little bit after diving into flex vs grid in CSS. The container got rewritten this way:

    <section class="wrapper">
        <ul class="colList">
            @foreach (var kvp in Model.ResultList.OrderBy(a => a.Key))
            {
                <li class="columnItem">
                    <div class="columnTitle--wrapper">
                        <h2>&gt;&gt;&gt; @kvp.Key</h2>
                    </div>
                    <div class="cardViewer">
                        <div class="cardList">
                            <ul class="cardList--ul">
                                @foreach (var subject in kvp.Value.OrderBy(a => a.EventTitle).ThenBy(a => a.LastName).ThenBy(a => a.SubjectId))
                                {
                                    <li class="cardItem">
                                        <h3>@subject.EventTitle.ToUpper()</h3>
                                        <h3>@subject.LastName.ToUpper()</h3>
                                        <h3>#@subject.SubjectId.ToUpper()</h3>
                                    </li>
                                }
                            </ul>
                        </div>
                    </div>
                </li>
            }
        </ul>
    </section>
    

    and the CSS turned into:

    /* column styles */
    .wrapper {
    }
    
    .colList {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
        grid-gap: .5rem;
        align-items: start;
        top: 500vh;
    }
    
    .columnItem {
        border-radius: .2rem;
        padding: .5rem;
    }
    
    .columnTitle--wrapper {
    }
    
        .columnTitle--wrapper h2 {
            font-weight: 700;
        }
    
    /* card styles */
    .cardViewer {
        overflow: hidden;
        height: 80vh;
        max-height: 80vh;
    }
    
    .cardList {
        position: relative;
    }
    
    .cardList--ul {
        display: grid;
        grid-template-rows: auto;
        grid-gap: .5rem;
        margin: .5rem 0;
    }
    
    .cardItem {
        background-color: white;
        border: 1px solid #BBB;
        border-radius: .25rem;
        box-shadow: 0 1px 0 rgba(9,45,66,.25);
        padding: .5rem;
    
        display: grid;
        grid-template-columns: 5fr 7fr 2fr;
    }
    

    And I added JS to handle multiple marquees when needed (list was too long for the viewport):

    class marqueeInfo {
        constructor(viewer, cardList) {
            this.viewer = viewer;
            this.cardList = cardList;
    
            if (this.isScrollable) {
                this.cardList.style.top = parseInt(this.viewerHeight()) + "px"
            }
            else {
                this.cardList.style.top = "0px"
            }
        }
    
        viewer() { return this.viewer; }
        cardList() { return this.cardList; }
        viewerHeight() { return this.viewer.offsetHeight; }
        cardListHeight() { return this.cardList.offsetHeight; }
        isScrollable() { return this.cardList.offsetHeight > this.viewer.offsetHeight; }
    }
    
    var marqueeArray = [];
    var marqueeSpeed = 4    //Specify marquee scroll speed (larger is faster 1-10)
    var delayb4scroll = 100 //Specify initial delay before marquee starts to scroll on page (2000=2 seconds)
    var lastRefresh;
    var isRefreshing = false;
    var refreshMinutes = 1;
    
    // may be overkill here...
    if (window.addEventListener)
        window.addEventListener("load", initializeMarqueeHandler, false)
    else if (window.attachEvent)
        window.attachEvent("onload", initializeMarqueeHandler)
    else if (document.getElementById)
        window.onload = initializeMarqueeHandler
    
    
    function initializeMarqueeHandler() {
        lastRefresh = new Date();
        isRefreshing = false;
        var viewers = document.getElementsByClassName("cardViewer");
        var cards;
        var i;
        for (i = 0; i < viewers.length; i++) {
            cards = viewers[i].getElementsByClassName("cardList");
            if (cards.length != 1)
                return;
            marqueeArray.push(new marqueeInfo(viewers[i], cards[0]));
        }
        setTimeout('lefttime=setInterval("scrollMarquees()",30)', delayb4scroll)
    }
    
    function scrollMarquees() {
        marqueeArray.forEach(function (marquee, index, array) {
            if (marquee.isScrollable()) {
                var cardHeight = marquee.cardListHeight();
                var viewerHeight = marquee.viewerHeight();
                var cardTop = parseInt(marquee.cardList.style.top);
                var targetTop = 0 - cardHeight;
                if (cardTop > targetTop)  // are we thru the list?
                    marquee.cardList.style.top = cardTop - marqueeSpeed + "px" //scroll
                else
                    marquee.cardList.style.top = viewerHeight + 25 + "px";
            }
            else {
                marquee.cardList.style.top = "0px";
            }
        })
    
        var milliseconds = (new Date()).getTime() - lastRefresh.getTime();
        if (milliseconds > (refreshMinutes * 60 * 1000) && !isRefreshing) {
            lastRefresh = new Date();
            isRefreshing = true;
            window.location.reload(true);
        }
    }
    

    Everything is working as expected - finally.