Simple Fix Complicated Execution
I worked out a simple solution to truncate my list of topics using only CSS. It's the only part of my website that really relies on JavaScript. It's wouldn't really be a heavy load for a script to run, but I didn't want to iterate over elements and individually hide them by applying a class to elements one at a time. It seemed like an unnecessary amount of computation, and I thought there was a lighter way to do it.
I was right, there was. But there were issues due to design choices I had already made. Being a hobbyist, I enjoy the liberty of doing what I want when it comes to these things. Even if what I want to do might be a little stupid.
I don't seek out cutting edge technologies when I code my website, but I do use modern solutions where they are well supported. I won't ensure support for old versions of browsers. If it runs on my installation of Debian stable, but it doesn't work on your browser, then you might need to consider updating whatever software you are running.
Another rule I follow for my own website is no layers of obfuscation. That is vanilla HTML, CSS and JavaScript all the way. There's a few reasons why.
- It's more fun. I can solve problems myself and learn a whole heap in the process.
- I'm not making anything large or complex enough to warrant React or SASS.
- Dependencies? Node thank you!
So, what have I learned about the state of the internet today? Well, it's doing pretty well. HTML does way more than I originally knew about. That was already the case, but scripted solutions are so commonplace that raw HTML concepts and good practises are often glossed over, if even mentioned at all. CSS can do a lot of the stuff now that people turn to preprocessors for out of the gate. And vanilla JavaScript can hold it's own against JQuery now in any use case that I have encountered. It's the bell curve in action.
So my task was to truncate an unordered list, at say, the 6th or so list element - out of mercy for users of accessible technology. I was already visually cropping the unordered list, but every list element remained in the accessibility tree, meaning someone who is keyboard bound would have to hit tab 37 times to get past my filter list. So I just used an nth-of-type CSS rule when a parent had a class applied to indicate truncation. Later I added a rule to override the previous one for the selected filter if it exceeded the truncation index defined by n.
search#topics {
& ul {
&.truncated li {
&:nth-of-type(n+7) {
display: none;
}
&:has(button[aria-pressed="true"]) {
display: inline;
}
}
}
}
Nice, right? Now, I don't want to be defining the truncation index in CSS and JavaScript separately - having the same thing defined in two places is just asking for a bug to happen when one is changed and the other forgotten. So I don't want this hard-coded in the JavaScript too. Helpfully, any loaded style sheets are available in the document object. Unhelpfully, CSS variables can't be used within selector functions such asĀ nth-of-type. SASS now? No. I'm as stubborn as I am stupid.
The filter search is the only part of my website that currently depends on JavaScript. I've intentionally kept script to a minimum. As the filter search won't work without script, I hide all it's contents by default except for the noscript element using a class called noscript.
search#topics {
&.noscript > *:not(noscript) {
display: none;
}
}
Then, once the document has loaded, the first execution in the JavaScript unhides it all.
document.querySelector('search#topics').classList.remove('noscript');
The rest of the script does the following jobs:
- Makes a
setof unique topics based on topics that are associated to articles. - Populate the unordered list with list elements for each topic, each containing a button with an event listener to trigger a filter function.
- Check if the number of topics exceeds the topic truncation index, and apply the truncated class to the unordered list if so.
- If truncation is required, create a button to toggle truncation on and off.
- Update user messages whenever anything changes regarding filters or truncation.
As a preference, I like to cache the DOM once, and then pass the required data between functions. I avoid using global variables, and will let the document load before executing any script just to be sure not to encounter any DOM loading issues.
Now, with that in mind, how to get the truncation index used in the CSS selector? Style sheets are included in the document and have a cssRules property each that is defined by it's selector. That's great. But I like to use nesting to make my life easier, and the styleSheets object doesn't distinguish nesting. CSS nesting likely didn't exist when this JavaScript object was first made. Darn. We get access to the base selector search#topics, everything within that is parsed cssText. Okay, that's a hindrance, but like I said I am stubborn.
So what we have now is a getTruncIndex function, that calls a getNumberAfterSubstring function that I borrowed from Stack Overflow because I've been working on this way too long at this point and I can't be bothered to work out the correct regular expression myself.
The next challenge was figuring out if a selected topic fell outside of the truncation limit. If the topic at index 8 is selected, but only the first 6 topics are shown during truncation, then a user is not going to be able to deselect the filter, or even see which topic has is being filtered.
My first idea here was to check if any topic buttons after the truncation index had their display style set to inline, but for some reason, in the document model, all topics had a display attribute of null. I'm not really sure why that is or what's going on there. But fine, I'll find another way.
What I ended up doing was a little more complicated, but I think I'm happy with regardless. TopicList is the unordered list element, we run a query to check if any buttons are currently pressed by checking their aria-pressed attribute. My filter search only filters one topic at a time, so I can be confident of accuracy here. Next, the slice point is defined by checking if there is currently an active filter, and if there is, checking it's index in the original topics array. The slice is then defined based on whether or not the active filter has a greater index the the truncation index.
function updateTruncMsg(topicList, topics, truncIndex) {
const truncMsg = document.querySelector('#usr-msg--truncated');
const activeFilter = topicList.querySelector('button[aria-pressed="true"]');
const slice = activeFilter &&
topics.indexOf(activeFilter.innerText) > truncIndex - 2 ?
truncIndex : truncIndex - 1;
if (topicList.classList.contains('truncated')) {
truncMsg.innerText = `Showing ${slice} of ${topics.length} topics`;
} else {
truncMsg.innerText = `Showing ${topics.length} of ${topics.length} topics`;
}
};
So by making things easier for myself, and unknowingly made things harder for myself at the same time. Nonetheless, I won't stop nesting CSS so long as the Epiphany browser keeps running my code without bugs.