I'm working on a calendar element whereby the day events are utilising grid row to span over their specified time frame.
However, I seem to be struggling when it comes to setting the event widths. By default they will take up the full width of the day when it's the only event at that time. And if they are concurring events at any point they will reduce their widths accordingly so they can all fit.
My problem occurs when I have a group of events happening at the same time, which generates multiple columns, and then an event later in the day which doesn't have any coinciding events. This event is now limited to the widths of the columns created from the events earlier in the day.
See image for context: Image of annotated demo
See below for a working demo.
(function() {
function init() {
let calendarElement = document.getElementsByClassName('calendar')[0],
events = calendarElement.getElementsByClassName('calendar__event');
_positionEventsOnGrid(events);
}
function _positionEventsOnGrid(events) {
Array.from(events).forEach(event => {
let gridRow = event.getAttribute('data-grid-row');
if (gridRow) {
let gridRowSpan = event.getAttribute('data-grid-row-span');
event.style.gridRow = gridRowSpan ? `${gridRow} / span ${gridRowSpan}` : gridRow;
}
});
}
init();
})();
body {
margin: 0;
block-size: 100vh;
inline-size: 100vw;
}
.calendar {
$block: &;
inline-size: 100%;
block-size: 100%;
display: grid;
grid-template-columns: 2.5rem 1fr;
grid-template-rows: auto 1fr;
}
.calendar__dayNames {
grid-row: 1;
grid-column: 2;
display: flex;
}
.calendar__dayName {
flex-grow: 1;
flex-basis: 0;
}
.calendar__schedule {
grid-row: 2;
grid-column: 1/span 2;
display: grid;
grid-template-columns: 2.5rem 1fr;
border-block-start: 1px solid #000;
}
.calendar__timeline {
display: grid;
grid-template-rows: repeat(19, 1.375rem);
gap: 0 0.625rem;
grid-column: 1/ span 2;
grid-row: 1;
position: relative;
}
.calendar__timelineItem {
display: flex;
align-items: center;
border-block-end: 1px dotted #222;
}
.calendar__timelineItem:nth-child(even) {
border-block-end: 1px solid #000;
}
.calendar__timelineItem:nth-child(odd):after {
display: inline;
content: attr(data-time);
}
.calendar__dayEventsContainer {
position: relative;
grid-row: 1;
grid-column: 2;
display: flex;
}
.calendar__dayEvents {
display: grid;
grid-template-rows: repeat(19, 1.375rem);
gap: 0 0.625rem;
border-inline-start: 1px solid #000;
flex-grow: 1;
flex-basis: 0;
}
.calendar__event {
background-color: #346DA8;
border: none;
}
<div class="calendar">
<div class="calendar__dayNames">
<div class="calendar__dayName">Monday</div>
<div class="calendar__dayName">Tuesday</div>
<div class="calendar__dayName">Wednesday</div>
<div class="calendar__dayName">Thursday</div>
<div class="calendar__dayName">Friday</div>
</div>
<div class="calendar__schedule">
<div class="calendar__timeline">
<div class="calendar__timelineItem" data-time="09:00"></div>
<div class="calendar__timelineItem" data-time="09:15"></div>
<div class="calendar__timelineItem" data-time="09:30"></div>
<div class="calendar__timelineItem" data-time="09:45"></div>
<div class="calendar__timelineItem" data-time="10:00"></div>
<div class="calendar__timelineItem" data-time="10:15"></div>
<div class="calendar__timelineItem" data-time="10:30"></div>
<div class="calendar__timelineItem" data-time="10:45"></div>
<div class="calendar__timelineItem" data-time="11:00"></div>
<div class="calendar__timelineItem" data-time="11:15"></div>
<div class="calendar__timelineItem" data-time="11:30"></div>
<div class="calendar__timelineItem" data-time="11:45"></div>
<div class="calendar__timelineItem" data-time="12:00"></div>
<div class="calendar__timelineItem" data-time="12:15"></div>
<div class="calendar__timelineItem" data-time="12:30"></div>
<div class="calendar__timelineItem" data-time="12:45"></div>
<div class="calendar__timelineItem" data-time="13:00"></div>
<div class="calendar__timelineItem" data-time="13:15"></div>
<div class="calendar__timelineItem" data-time="13:30"></div>
</div>
<div class="calendar__dayEventsContainer">
<div class="calendar__dayEvents">
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="6"></button>
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="3"></button>
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="1"></button>
<button type="button" class="calendar__event" data-grid-row="10" data-grid-row-span="1"></button>
</div>
<div class="calendar__dayEvents">
</div>
<div class="calendar__dayEvents">
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="6"></button>
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="1"></button>
</div>
<div class="calendar__dayEvents">
</div>
<div class="calendar__dayEvents">
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="1"></button>
</div>
</div>
</div>
</div>
I have tried a few things: grid-column: 1/-1 but this will do it to all the events and therefore incorrectly format the concurrent events.
grid-auto-flow: dense however, this didn't seem to do the trick in my example.
grid-template-columns: repeat(auto-fit, minmax()), but the min value as part of the minmax needs to be a fixed size.
I do wonder whether I need to extend the positionEventsOnGrid() function so it checks whether 'grid-column: 1/-1' can be applied to specific events. If it isn't coinciding with other events at that time. However, I feel this might be quite complex so thought I'd ask around in case there was a simpler way.
I'm wanting singular events to span the full width of their day, regardless of whether there are concurrent events earlier or later in the day.
The grid columns in your solution are implicitly generated because your CSS does not explicitly set a column layout. See @Michael Benjamin's answer here.
The problem is that numeric indexes (1 / -1) only work with explicit grids. In other words, grids with defined columns and rows. You're using an implicit grid, where columns and rows are automatically generated, as needed.
event.style.gridColumn = 1 / -1;
applied to a .calendar__event
element stretches across all columns if you set grid-template-columns: repeat()
to the CSS rule for .calendar__dayEvents
container. This is similar to your comment "the min value as part of the minmax needs to be a fixed size."
I do wonder whether I need to extend the positionEventsOnGrid() function so it checks whether 'grid-column: 1/-1' can be applied to specific events. If it isn't coinciding with other events at that time. However, I feel this might be quite complex so thought I'd ask around in case there was a simpler way.
I have not found a way around this. Fortunately, there are existing solutions on SO that can group overlapping time or integer intervals. In my calendar components, I've applied/adapted the function from GitHub that I posted on the linked SO post. The adaption is applied to the code snippet below.
The following code snippet does the following:
data-type
attribute.JS execution
event.style.gridRow
.window
function getComputedStyle()
, as provided by this SO post: count columns in a responsive grid..calendar__dayEvents
grid container
and set non-overlapping event items to event.style.gridColumn = 1 / -1;
.event.style.gridColumn = 1 / span ${gridColumnCount};
instead of setting the number of grid columns to the
grid-template-columns
event grid
container. (See comments in code snippet.)(function() {
function init() {
//const calendarElement = document.getElementsByClassName('calendar')[0];
//const events = calendarElement.getElementsByClassName('calendar__event');
//_positionEventsOnGrid(events);
const dayEventsElems = document.querySelectorAll('.calendar__dayEvents');
_positionEventsOnGrid2(dayEventsElems);
}
/*
function _positionEventsOnGrid(events) {
Array.from(events).forEach(event => {
let gridRow = event.getAttribute('data-grid-row');
if (gridRow) {
let gridRowSpan = event.getAttribute('data-grid-row-span');
event.style.gridRow = gridRowSpan ? `${gridRow} / span ${gridRowSpan}` : gridRow;
}
});
}
*/
function _positionEventsOnGrid2(dayEventsElems) {
if (!dayEventsElems) {
return;
}
dayEventsElems.forEach(dayEventElem => {
const events = dayEventElem.querySelectorAll('.calendar__event');
// if there are no event elements in the
// '.calendar__dayEvents' element then exit function.
if (events.length === 0) {
return;
}
// Grid layout using "implicit grid, where columns and rows
// are automatically generated, as needed."
// Source: https://stackoverflow.com/a/75250349
// Also see: https://stackoverflow.com/a/73624201
const overlappingGroups = getOverlappingEventGroups(events);
overlappingGroups.forEach(group => {
group.forEach(event => {
event.style.gridRow = `${event.startRow} / ${event.endRow}`;
});
});
// After the grid is laid out, count the number of columns
// created using the built-in 'gridComputedStyle()'.
// See the function 'getGridRowColCount()' below.
const gridElem = events[0].closest('.calendar__dayEvents');
// https://www.javascripttutorial.net/javascript-return-multiple-values
const [gridRowCount, gridColumnCount] = getGridRowColCount(gridElem);
gridElem.style.gridTemplateColumns = `repeat(${gridColumnCount}, 1fr)`;
overlappingGroups.forEach(group => {
// If the event group does not contain one entry,
// then exit function.
if (group.length !== 1) {
return;
}
// Otherwise, explicitly set the 'gridColumn'
// CSS style property to '1 / -1' so that the event
// element spans all columns in the 'gridElem' grid.
group[0].style.gridColumn = `1 / -1`;
// Or set the span value and remove the code above that
// sets the CSS property 'gridTemplateColumns'.
//group[0].style.gridColumn = `1 / span ${gridColumnCount}`;
});
});
}
// Source: https://stackoverflow.com/questions/55204205/a-way-to-count-columns-in-a-responsive-grid
function getGridRowColCount(grid) {
const gridComputedStyle = window.getComputedStyle(grid);
// get number of grid rows
const gridRowCount = gridComputedStyle.getPropertyValue("grid-template-rows").split(" ").length;
// get number of grid columns
const gridColumnCount = gridComputedStyle.getPropertyValue("grid-template-columns").split(" ").length;
//console.log(gridRowCount, gridColumnCount);
// https://www.javascripttutorial.net/javascript-return-multiple-values
return [gridRowCount, gridColumnCount];
}
/*
https://stackoverflow.com/a/75486209
Format of intervals array from:
https://gist.githubusercontent.com/blasten/acfaafc8247e37abf23f/raw/c71f42428e34dbb7ff2d1b5e932fdf4152fe1ad2/group-intervals.js
const intervals = [
[2, 5],
[5, 6],
[3, 4],
[7, 8],
[6.5, 9],
[10, 11.5]
];
The function 'getOverlappingEventGroups()' returns a
modified intervals array format containing '.calendar__event'
DOM elements with custom 'startRow' and 'endRow' properties.
*/
function getOverlappingEventGroups(events) {
const eventIntervals = [];
events.forEach(event => {
const gridRow = event.getAttribute('data-grid-row');
const gridRowSpan = event.getAttribute('data-grid-row-span');
if (isNaN(gridRow) || isNaN(gridRowSpan)) {
return;
}
const startRow = parseInt(gridRow, 10);
const endRow = startRow + parseInt(gridRowSpan, 10);
event.startRow = startRow;
event.endRow = endRow;
eventIntervals.push(event);
});
const eventGroups = groupOverlapingEventIntervals(eventIntervals);
//console.log("Overlapping event groups: ", eventGroups);
return eventGroups;
}
// Source: https://gist.githubusercontent.com/blasten/acfaafc8247e37abf23f/raw/c71f42428e34dbb7ff2d1b5e932fdf4152fe1ad2/group-intervals.js
function groupOverlapingEventIntervals(eventIntervals) {
eventIntervals.sort((a, b) => a.startRow - b.startRow);
const groups = [
[eventIntervals[0]]
];
let j = 0;
let end = eventIntervals[0].endRow;
for (let i = 1; i < eventIntervals.length; i++) {
if (eventIntervals[i].startRow <= end) {
if (eventIntervals[i].endRow > end) {
end = eventIntervals[i].endRow;
}
groups[j].push(eventIntervals[i]);
} else {
groups.push([eventIntervals[i]]);
j++;
end = eventIntervals[i].endRow;
}
}
return groups;
}
init();
})();
body {
margin: 0;
block-size: 100vh;
inline-size: 100vw;
}
.calendar {
inline-size: 100%;
block-size: 100%;
display: grid;
grid-template-columns: 2.5rem 1fr;
grid-template-rows: auto 1fr;
}
.calendar__dayNames {
grid-row: 1;
grid-column: 2;
display: flex;
}
.calendar__dayName {
flex-grow: 1;
flex-basis: 0;
}
.calendar__schedule {
grid-row: 2;
grid-column: 1/span 2;
display: grid;
grid-template-columns: 2.5rem 1fr;
border-block-start: 1px solid #000;
}
.calendar__timeline {
display: grid;
grid-template-rows: repeat(19, 1.375rem);
gap: 0 0.625rem;
grid-column: 1/ span 2;
grid-row: 1;
position: relative;
}
.calendar__timelineItem {
display: flex;
align-items: center;
border-block-end: 1px dotted #222;
}
.calendar__timelineItem:nth-child(even) {
border-block-end: 1px solid #000;
}
.calendar__timelineItem:nth-child(odd):after {
display: inline;
content: attr(data-time);
}
.calendar__dayEventsContainer {
position: relative;
grid-row: 1;
grid-column: 2;
display: flex;
}
.calendar__dayEvents {
display: grid;
grid-template-rows: repeat(19, 1.375rem);
gap: 0 0.625rem;
border-inline-start: 1px solid #000;
flex-grow: 1;
flex-basis: 0;
}
.calendar__event {
background-color: #346DA8;
border: none;
}
/* Color-coded event elements */
.calendar__event[data-type=aaa] {
background-color: rgb(243 165 97 / 80%);
}
.calendar__event[data-type=bbb] {
background-color: rgb(80 142 77 / 80%);
}
.calendar__event[data-type=ccc] {
background-color: rgb(228 53 53 / 80%);
}
.calendar__event[data-type=ddd] {
background-color: rgb(96 177 218 / 80%);
}
<div class="calendar">
<div class="calendar__dayNames">
<div class="calendar__dayName">Monday</div>
<div class="calendar__dayName">Tuesday</div>
<div class="calendar__dayName">Wednesday</div>
<div class="calendar__dayName">Thursday</div>
<div class="calendar__dayName">Friday</div>
</div>
<div class="calendar__schedule">
<div class="calendar__timeline">
<div class="calendar__timelineItem" data-time="09:00"></div>
<div class="calendar__timelineItem" data-time="09:15"></div>
<div class="calendar__timelineItem" data-time="09:30"></div>
<div class="calendar__timelineItem" data-time="09:45"></div>
<div class="calendar__timelineItem" data-time="10:00"></div>
<div class="calendar__timelineItem" data-time="10:15"></div>
<div class="calendar__timelineItem" data-time="10:30"></div>
<div class="calendar__timelineItem" data-time="10:45"></div>
<div class="calendar__timelineItem" data-time="11:00"></div>
<div class="calendar__timelineItem" data-time="11:15"></div>
<div class="calendar__timelineItem" data-time="11:30"></div>
<div class="calendar__timelineItem" data-time="11:45"></div>
<div class="calendar__timelineItem" data-time="12:00"></div>
<div class="calendar__timelineItem" data-time="12:15"></div>
<div class="calendar__timelineItem" data-time="12:30"></div>
<div class="calendar__timelineItem" data-time="12:45"></div>
<div class="calendar__timelineItem" data-time="13:00"></div>
<div class="calendar__timelineItem" data-time="13:15"></div>
<div class="calendar__timelineItem" data-time="13:30"></div>
</div>
<div class="calendar__dayEventsContainer">
<div class="calendar__dayEvents">
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="6"></button>
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="3"></button>
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="1"></button>
<button type="button" class="calendar__event" data-grid-row="10" data-grid-row-span="1"></button>
</div>
<div class="calendar__dayEvents">
<button type="button" class="calendar__event" data-grid-row="3" data-grid-row-span="7" data-type="bbb"></button>
<button type="button" class="calendar__event" data-grid-row="12" data-grid-row-span="3" data-type="aaa"></button>
<button type="button" class="calendar__event" data-grid-row="14" data-grid-row-span="2" data-type="ccc"></button>
</div>
<div class="calendar__dayEvents">
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="6"></button>
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="1"></button>
</div>
<div class="calendar__dayEvents">
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="6" data-type="aaa"></button>
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="3" data-type="bbb"></button>
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="1" data-type="aaa"></button>
<button type="button" class="calendar__event" data-grid-row="2" data-grid-row-span="1" data-type="ccc"></button>
<button type="button" class="calendar__event" data-grid-row="2" data-grid-row-span="4" data-type="bbb"></button>
<button type="button" class="calendar__event" data-grid-row="10" data-grid-row-span="1" data-type="ddd"></button>
<button type="button" class="calendar__event" data-grid-row="14" data-grid-row-span="3" data-type="ddd"></button>
</div>
<div class="calendar__dayEvents">
<button type="button" class="calendar__event" data-grid-row="1" data-grid-row-span="1"></button>
</div>
</div>
</div>
</div>
Update (9/26/2023)
Added image with annotations as an alternative code execution description.
.calendar__dayEvents
div containers (that is, each day-of-week column outlined in yellow)..calendar__event
elements with overlapping intervals (outlined in red boxes)..calendar__event
elements within the .calendar__dayEvents
div container/day-of-week column..calendar__event
element in overlap groups with only 1 .calendar__event
entry.calendar__dayEvents
div container and set .calendar__event
element in overlap groups with only 1 .calendar__event
entry to span all columns (via style.gridColumn = 1 / -1
).