Styling cells that span multiple rows

I'm attempting to build a timetable using HTML tables and apply a class that visually indicates the current event using a bit of js. Each row represents 30 minutes, and different events last for different amounts of time. I've worked out how to apply the styles to cells in a given row, and worked out some logic for selecting the right <tr> based on the current time, but longer events span multiple rows and only receive the styling while the first 30-minute chunk (i.e. the row that the cell starts in) has the class applied. Is there any way to apply a class/style to a cell by applying styles to one of the rows it spans? If not, any advice on the cleanest way to apply styles to those cells? Right now, the best I can come up with is putting a data attribute on every cell for the time window its in and comparing it to the current time, which feels a bit ridiculous. Thanks! https://codepen.io/Beanie127/pen/dyxXyLO
99 Replies
ἔρως
ἔρως2mo ago
what is your expected result? from the text, i can't imagine it
Alex
AlexOP2mo ago
So, Breakfast spans 3 rows, 07:00 through 08:00. While the time is between 07:00 and 08:30, I want to be able to apply the class .current-time to the cell. Right now, if the time is between 07:00 and 07:29, the class is applied to all cells in the row 07:00, so the cell containing Breakfast is highlighted, but if the time is 07:30, even though the cell for Breakfast spans that row, it doesn't receive the style, because the <td> isn't actually in that row. I want to know if there's a clever way to apply a style to that cell, or to cells that span a given row, even though the cell isn't actually in that row.
ἔρως
ἔρως2mo ago
i see what you mean
Alex
AlexOP2mo ago
I hate working with time
ἔρως
ἔρως2mo ago
uh ... where's the data? is there any reason why you don't have this in javascript?
Alex
AlexOP2mo ago
I've attempted to build a version of this in JS; my data management skills are such that I can't work out the best way to notate it. Each workshop session actually has multiple different options within it, some but not all of which are repeated at different points throughout the event, which spans several days, and there are a bunch of locations to consider as well. If possible, I just find it so much easier to write it all in HTML rather than try and build a data architecture to represent it all in JS, just to then render HTML again. The actual data currently exists as a bunch of spreadsheets and text documents
ἔρως
ἔρως2mo ago
the problem is that now you have to read the data from the dom which is ... eh i think you should start over on your code, by the way
Alex
AlexOP2mo ago
I'm open to suggestions; what issues do you find with the code?
ἔρως
ἔρως2mo ago
for starters, (hours - 7) * 2 you also remove the class and add it back again tot he same element also, you have a syntax error
Alex
AlexOP2mo ago
That should be (hours - startTime) * 2, because the day runs in 30 min increments
ἔρως
ἔρως2mo ago
the console said this: 🤔
No description
Alex
AlexOP2mo ago
It removes the class from all elements first, so that when the time updates, it doesn't remain on the previous element
ἔρως
ἔρως2mo ago
class updates should be done at the end, because you can trigger a re-draw
Alex
AlexOP2mo ago
I don't understand what that means, sorry
ἔρως
ἔρως2mo ago
it means that, if you remove the class, if you're unlucky, you can cause a redraw on the page which has performance implications also, you could be changing the class of the element to the same element also, you could start checking from the first element that's marked as current-time, and have some speedups and simplify your code
Alex
AlexOP2mo ago
I understand that this can be optimised, certainly. This is minimum viable product to demonstrate the effect I'm trying to achieve. My question is more about how to style the relevant cell
ἔρως
ἔρως2mo ago
so, the code is working fine?
Alex
AlexOP2mo ago
Whatever time I set "hours" and "mins" to, this code reliably selects the row which matches that time and applies the class to it. But that doesn't affect cells which span that row. I want to know either if there is a way to select cells which span a given row, either via CSS or JS, so that I can style it.
ἔρως
ἔρως2mo ago
so, it isn't working fine?
Alex
AlexOP2mo ago
no
ἔρως
ἔρως2mo ago
then lets make it work start by deleting everything except the first line, on a different place
Alex
AlexOP2mo ago
of the JS, right?
ἔρως
ἔρως2mo ago
yes but do it somewhere else
Alex
AlexOP2mo ago
I've forked the pen
ἔρως
ἔρως2mo ago
good the idea is simple write a function that, given the timeTable as an argument and a current time (minutes and seconds, together or separated, however you wish) outputs the following: - 000:00 - 06:59 -> null - 07:00 - 23:59 -> node it can be the first child and no, im not writting this for you the idea is to return any event from the table
Alex
AlexOP2mo ago
function getNode(table, time) {
if (time.hours < 7) return null;
return table.firstChild
}
function getNode(table, time) {
if (time.hours < 7) return null;
return table.firstChild
}
ἔρως
ἔρως2mo ago
what about the minutes? oh, wait, you're passing an object with hours and minutes?
Alex
AlexOP2mo ago
yeah
ἔρως
ἔρως2mo ago
personally, i wouldn't do it like that, but that's fine now, give that function a proper name getNode is technically correct, but meaningless a better name is, for example, getCurrentEvent the name has to describe to the human reading what the function is for
Alex
AlexOP2mo ago
Understood
ἔρως
ἔρως2mo ago
now, i need a final decisions from you: is this going to have 1 and only 1 event for a time, forever?
Alex
AlexOP2mo ago
Yes
ἔρως
ἔρως2mo ago
okay, now, on a for ... of ... loop, show all elements in the console
Alex
AlexOP2mo ago
function logElements(elementList) {
for (element in list) {
console.log(elements);
}
}
function logElements(elementList) {
for (element in list) {
console.log(elements);
}
}
ἔρως
ἔρως2mo ago
no, in the same function everything im going to tell you, for a bit, is in the function
Alex
AlexOP2mo ago
function getCurrentEvent(table, time) {
if (time.hours < 7) return null;
const timeslots = table.children;
for (timeslot in timeslots) {
console.log(timeslot);
}
return table.firstChild;
}
function getCurrentEvent(table, time) {
if (time.hours < 7) return null;
const timeslots = table.children;
for (timeslot in timeslots) {
console.log(timeslot);
}
return table.firstChild;
}
ἔρως
ἔρως2mo ago
no, not an in https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of never ever ever use for( ... in ...) for arrays and array-like objects if you forget to set something as non-enumerable, it will show in the loop which is very very weird for arrays
Alex
AlexOP2mo ago
I'll have a proper look over that link later, but for now I'll take your word for it
ἔρως
ἔρως2mo ago
also, wth did i post that as code? 🤦 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of <-- this is an important read, so you understand the syntax
Alex
AlexOP2mo ago
should that be for (let timeslot in timeslots) then? Do I need to explicitly assign it as a variable?
ἔρως
ἔρως2mo ago
that's an in, not an of those are 2 different loops
Alex
AlexOP2mo ago
all the examples on MDN include a const declaration
ἔρως
ἔρως2mo ago
you can use any
Alex
AlexOP2mo ago
okay, cool
ἔρως
ἔρως2mo ago
const, let, var
Receives a value from the sequence on each iteration. May be either a declaration with const, let, or var, or an assignment target [...]
Alex
AlexOP2mo ago
okay, so we've got a reference to each row of the table and we're iterating over them and logging each one to console
ἔρως
ἔρως2mo ago
exactly this is just so you're familiar with how the loop works now, you need to create a constant outside the function that constant will have the minimum time the minimum time is 7 after that, replace the 7 in the function with that constant
Alex
AlexOP2mo ago
const minTime = 7;
function getCurrentEvent(table, time) {
if (time.hours < minTime) return;
const timeslots = table.children;
for (const timeslot of timeslots) {
console.log(timeslot)
}
return table.firstChild;
const minTime = 7;
function getCurrentEvent(table, time) {
if (time.hours < minTime) return;
const timeslots = table.children;
for (const timeslot of timeslots) {
console.log(timeslot)
}
return table.firstChild;
ἔρως
ἔρως2mo ago
that function needs space to breathe also, you're not returning null, you're returning undefined
Alex
AlexOP2mo ago
const minTime = 7;

function getCurrentEvent(table, time) {

if (time.getHours() < minTime) return null;

const timeslots = table.children;

for (const timeslot of timeslots) {
console.log(timeslot)
}

return table.firstChild;
}
const minTime = 7;

function getCurrentEvent(table, time) {

if (time.getHours() < minTime) return null;

const timeslots = table.children;

for (const timeslot of timeslots) {
console.log(timeslot)
}

return table.firstChild;
}
better?
ἔρως
ἔρως2mo ago
much better now, you need 2 variables: current time and current element the current time starts from minimum time and the current element starts at null
Alex
AlexOP2mo ago
const minTime = 7;

function getCurrentEvent(table, time) {

if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentTime = minTime;
let currentElement = null;

for (const timeslot of timeslots) {
console.log(timeslot)
}

return table.firstChild;
}
const minTime = 7;

function getCurrentEvent(table, time) {

if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentTime = minTime;
let currentElement = null;

for (const timeslot of timeslots) {
console.log(timeslot)
}

return table.firstChild;
}
ἔρως
ἔρως2mo ago
actually, we will have to do something different we will need the do { ... } while() loop instead i forgot to account for the minutes so, the idea is simple: if there's a 2nd child inside the row, that means there's an event there actually, if you want a for loop works too the idea is to increment the current time by 0.5, while it is lower than 24 and then, to get the index, you can subtract minTime and multiply by the number of events per hour (another constant to create outside of the function), which is 2 actually, that 0.5 needs to be a constant, because it is the time "step" - how much it moves forward so, the value of that constant is 1 / <num events per hour> and then, the current time increments by that step
Alex
AlexOP2mo ago
const minTime = 7;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentTimeIndex = minTime;
let currentElement = null;

for (const timeslot of timeslots) {
console.log(timeslot);
}

return table.firstChild;
}
const minTime = 7;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentTimeIndex = minTime;
let currentElement = null;

for (const timeslot of timeslots) {
console.log(timeslot);
}

return table.firstChild;
}
So this is what I have so far
ἔρως
ἔρως2mo ago
the for loop has to be changed for an actual for(...; ...; ...) loop i know i said to use that, but i made a mistake and forgot the minutes
Alex
AlexOP2mo ago
I'm slightly confused on what I'm iterating over; like this?
const minTime = 7;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentTime = minTime;
let currentElement = null;

for (currentTime; currentTime < 24; currentTime += timeStep) {
console.log(timeslot);
}

return table.firstChild;
}
const minTime = 7;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentTime = minTime;
let currentElement = null;

for (currentTime; currentTime < 24; currentTime += timeStep) {
console.log(timeslot);
}

return table.firstChild;
}
ἔρως
ἔρως2mo ago
you should move the variable declaration to the loop
Alex
AlexOP2mo ago
const minTime = 7;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (let currentTime = minTime; currentTime < 24; currentTime += timeStep) {
console.log(timeslot);
}

return table.firstChild;
}
const minTime = 7;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (let currentTime = minTime; currentTime < 24; currentTime += timeStep) {
console.log(timeslot);
}

return table.firstChild;
}
ἔρως
ἔρως2mo ago
i know it is a bit long now, the idea is to log the index so, the index has to start at 0, then 1, then 2 ... and the index is (currentTime - minTime) * eventsPerHour
Alex
AlexOP2mo ago
const minTime = 7;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (let currentTime = minTime; currentTime < 24; currentTime += timeStep) {
const index = (currentTime - minTime) * eventsPerHour;
console.log(index);
}

return table.firstChild;
}
const minTime = 7;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (let currentTime = minTime; currentTime < 24; currentTime += timeStep) {
const index = (currentTime - minTime) * eventsPerHour;
console.log(index);
}

return table.firstChild;
}
ἔρως
ἔρως2mo ago
is it working?
Alex
AlexOP2mo ago
seems to be!
ἔρως
ἔρως2mo ago
and it returns the indexes? should go all the way to 47 or 48 or something wait, no way less 33
Alex
AlexOP2mo ago
If I feed it a time of 7am, it returns indexes 0–33
ἔρως
ἔρως2mo ago
yes, that's correct now, log each first child of the <tr>
Alex
AlexOP2mo ago
console.log(timeslots[index].firstChild) gives me a bunch of
// [object Text]
{}
// [object Text]
{}
` followed by an error: cannot read properties of undefined
ἔρως
ἔρως2mo ago
and what did the timeslots[index] give you?
Alex
AlexOP2mo ago
every row, then 'undefined' presumably because the table ends at 23:00 and we're still going at 23.5
const minTime = 7;
const maxTime = 23.5;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (
let currentTime = minTime;
currentTime < maxTime;
currentTime += timeStep
) {
const index = (currentTime - minTime) * eventsPerHour;
console.log(timeslots[index]);
}

return table.firstChild;
}
const minTime = 7;
const maxTime = 23.5;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (
let currentTime = minTime;
currentTime < maxTime;
currentTime += timeStep
) {
const index = (currentTime - minTime) * eventsPerHour;
console.log(timeslots[index]);
}

return table.firstChild;
}
turned maxTime into a variable for clarity
ἔρως
ἔρως2mo ago
yup, maxtime is 23.5 and now, get the 2nd child if there isn't one, continue the loop if there's one, show in the console and also show the time that it is
Alex
AlexOP2mo ago
function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (
let currentTime = minTime;
currentTime < maxTime;
currentTime += timeStep
) {
const index = (currentTime - minTime) * eventsPerHour;
if (!timeslots[index].children[1]) continue;
console.log(timeslots[index].children[1], currentTime);
}

return table.firstChild;
}
function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (
let currentTime = minTime;
currentTime < maxTime;
currentTime += timeStep
) {
const index = (currentTime - minTime) * eventsPerHour;
if (!timeslots[index].children[1]) continue;
console.log(timeslots[index].children[1], currentTime);
}

return table.firstChild;
}
ἔρως
ἔρως2mo ago
instead of always writting that long thingy, you can store it into a variable you can call it element or event or something
Alex
AlexOP2mo ago
const minTime = 7;
const maxTime = 23.5;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (let t = minTime; t < maxTime; t += timeStep) {
const index = (t - minTime) * eventsPerHour;
const event = timeslots[index].children[1];
if (!event) continue;
console.log(event, t);
}

return table.firstChild;
}
const minTime = 7;
const maxTime = 23.5;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, time) {
if (time.getHours() < minTime) return null;

const timeslots = table.children;

let currentElement = null;

for (let t = minTime; t < maxTime; t += timeStep) {
const index = (t - minTime) * eventsPerHour;
const event = timeslots[index].children[1];
if (!event) continue;
console.log(event, t);
}

return table.firstChild;
}
also renamed currentTime because the actual current time is time.getHours() that we're passing into the function
ἔρως
ἔρως2mo ago
if you're going to use t, might as well use i because it is the variable usually used for a for loop it's just a convention, and you can keep using t it's perfectly fine so, now the idea is simple: you have to compare the time in the loop with the start time you have to calculate the end time of the event which is pretty easy: it's the value in rowspan multiplied by timeStep
Alex
AlexOP2mo ago
//...
for (let t = minTime; t < maxTime; t += timeStep) {
const index = (t - minTime) * eventsPerHour;
const event = timeslots[index].children[1];

if (!event) continue;

let eventDuration = event.getAttribute("rowspan") ?? 1;
let endTime = eventDuration * timeStep + t;
console.log(event, t, endTime);
}
//...
//...
for (let t = minTime; t < maxTime; t += timeStep) {
const index = (t - minTime) * eventsPerHour;
const event = timeslots[index].children[1];

if (!event) continue;

let eventDuration = event.getAttribute("rowspan") ?? 1;
let endTime = eventDuration * timeStep + t;
console.log(event, t, endTime);
}
//...
ἔρως
ἔρως2mo ago
and the end time is correct?
Alex
AlexOP2mo ago
well, it shows the time that the event ends, yes, but whether that's the number we actually need, I don't know, because that's the time that corresponds to the next timeslot
ἔρως
ἔρως2mo ago
that's expected if it starts at 7 and ends in 30 minutes, it will be 7.5
Alex
AlexOP2mo ago
in which case, yes, it's correct it's accurate 😛
ἔρως
ἔρως2mo ago
now, you need to convert the time from a date object into a float and you do it by doing hours + (60 / minutes) that value will be needed multiple times, so, cleate it outside the loop and it won't change, so, a constant
Alex
AlexOP2mo ago
function getCurrentEvent(table, currentTime) {
if (currentTime.getHours() < minTime) return null;

const currentTimeFloat = currentTime.getHours() + 60 * currentTime.getMinutes();

const timeslots = table.children;

let currentElement = null;

for (let t = minTime; t < maxTime; t += timeStep) {
const index = (t - minTime) * eventsPerHour;
const event = timeslots[index].children[1];

if (!event) continue;

let eventDuration = event.getAttribute("rowspan") ?? 1;
let endTime = eventDuration * timeStep + t;
console.log(event, t, endTime);
}

return table.firstChild;
}
function getCurrentEvent(table, currentTime) {
if (currentTime.getHours() < minTime) return null;

const currentTimeFloat = currentTime.getHours() + 60 * currentTime.getMinutes();

const timeslots = table.children;

let currentElement = null;

for (let t = minTime; t < maxTime; t += timeStep) {
const index = (t - minTime) * eventsPerHour;
const event = timeslots[index].children[1];

if (!event) continue;

let eventDuration = event.getAttribute("rowspan") ?? 1;
let endTime = eventDuration * timeStep + t;
console.log(event, t, endTime);
}

return table.firstChild;
}
ἔρως
ἔρως2mo ago
now, check if the current time is between the start and end time if isn't, continue the loop if it is, set currentElement to that <td> and break the loop
Alex
AlexOP2mo ago
if (!(t < currentTimeFloat < endTime)) continue; is that syntax right? I'm not sure about negating multiple conditions
ἔρως
ἔρως2mo ago
then return currentElement no, that's python syntax if you are okay with multiple ifs, then you can check if the current time is lower than the start time and then check if the end time is higher than the current time
Alex
AlexOP2mo ago
Is there a reason I shouldn't just do this and otherwise leave the loop to continue?
if (t < currentTimeFloat < endTime) {
currentElement = event;
break;
}
if (t < currentTimeFloat < endTime) {
currentElement = event;
break;
}
ἔρως
ἔρως2mo ago
yes: that doesn't really exist, as far as i know yeah, it always returns true you can check if the current time is equal or higher than the loop time and then check if the current time is lower or equal to the end time
Alex
AlexOP2mo ago
const minTime = 7;
const maxTime = 23.5;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, currentTime) {
if (currentTime.getHours() < minTime) return null;

const currentTimeFloat =
currentTime.getHours() + 60 * currentTime.getMinutes();

const timeslots = table.children;

let currentElement = null;

for (let startTime = minTime; startTime < maxTime; startTime += timeStep) {
const index = (startTime - minTime) * eventsPerHour;
const event = timeslots[index].children[1];

if (!event) continue;

let eventDuration = event.getAttribute("rowspan") ?? 1;
let endTime = eventDuration * timeStep + startTime;

if (currentTime >= startTime) {
if (currentTime =< endTime) {
currentElement = event;
break;
}
}
}

return currentEvent;
}
const minTime = 7;
const maxTime = 23.5;
const eventsPerHour = 2;
const timeStep = 1 / eventsPerHour;

function getCurrentEvent(table, currentTime) {
if (currentTime.getHours() < minTime) return null;

const currentTimeFloat =
currentTime.getHours() + 60 * currentTime.getMinutes();

const timeslots = table.children;

let currentElement = null;

for (let startTime = minTime; startTime < maxTime; startTime += timeStep) {
const index = (startTime - minTime) * eventsPerHour;
const event = timeslots[index].children[1];

if (!event) continue;

let eventDuration = event.getAttribute("rowspan") ?? 1;
let endTime = eventDuration * timeStep + startTime;

if (currentTime >= startTime) {
if (currentTime =< endTime) {
currentElement = event;
break;
}
}
}

return currentEvent;
}
ἔρως
ἔρως2mo ago
and not 2 ifs the result is the same, but is lower brace hell
Alex
AlexOP2mo ago
if (currentTime >= startTime && currentTime =< endTime) {
currentElement = event;
break;
}
if (currentTime >= startTime && currentTime =< endTime) {
currentElement = event;
break;
}
ἔρως
ἔρως2mo ago
that's better now, test the function
Alex
AlexOP2mo ago
doesn't seem to be working I had the currentTImeFloat calculation wrong, was multplying instead of dividing; I fixed that, and it still doesn't return
ἔρως
ἔρως2mo ago
🤔
Alex
AlexOP2mo ago
because I'm comparing currentTime instead of currentTimeFloat, fuck me
ἔρως
ἔρως2mo ago
yup
Alex
AlexOP2mo ago
that did it
ἔρως
ἔρως2mo ago
i actually didn't notice it
Alex
AlexOP2mo ago
dunno where you are but it's 4am here in the UK, that's certainly a factor for me
ἔρως
ἔρως2mo ago
but does it return the right element? it's a good time to sleep it took a while to re-write it
Alex
AlexOP2mo ago
it doesn't but it is returning an element and I can troubleshoot later thank you very much for your help, this has been incredibly informative
ἔρως
ἔρως2mo ago
you're welcome
Want results from more Discord servers?
Add your server