Site
My personal site, which you are currently browsing, is made using Jekyll. Jekyll is one of many site generator (SG): it takes a bunch of files, in this case markdown files, and then converts them to a HTML page. The resulting HTML page can then be hosted virtually anywhere.
The reason why I am using a SG is because creating content in markdown is easy. It takes very little effort to make a site, and little maintenance. To read more about the philosophy of Jekyll see its page on the jamstack site.
Creating the site was fairly enjoyable. While most things are supported by the theme I use, minimal mistakes, the grid of posts on the notes page is a bunch of custom stuff.
From top to bottom, first are some buttons to filter on categories. These buttons are generated using Liquid templating. All buttons are part of a main container, button-group
, so I can use flexbox for layouting. Each category gets its own button, which is by default active
, and has a data-filter
attribute that is used for filtering.
<!-- Buttons to filter -->
<div class="button-group filter-button-group justify-content-center">
{% for category in site.categories %}
<a class="btn btn-sm btn-primary active" data-filter="{{ category[0] }}">{{ category[0] }}</a>
{% endfor %}
</div>
Below the buttons is the grid, which is also generated using Liquid: for each post I create a post-item
that is shown by default (hence post-shown
). At the bottom of the grid is a piece of text, identified by #emptygrid
, that is hidden by default. This shows up if you filter out all posts.
<!-- Grid -->
<div class="grid flex">
{% for post in site.posts %}
<div class="post-item post-shown {{ post.category }}">
<a class="post-link" href="{{ post.url }}">
<h4>{{ post.title }}</h4>
</a>
<p> {{ post.excerpt | markdownify }}</p>
</div>
{% endfor %}
<p id="emptygrid" style="display:none">You have filtered out all posts. It is empty now.</p>
</div>
Filtering of these items is achieved through some JS. First, grab the values that we can filter on.
// Create a list of values that we filter on.
let filterValues = []
let elems = document.querySelectorAll('.filter-button-group a.active');
for (const elem of elems)
// This grabs the data-filter attribute of the buttons made above.
filterValues.push(elem.getAttribute("data-filter"))
Then, add an event listener to all buttons. This event listener is the code that runs once you click on the button.
// Loop over all the buttons
const buttons = document.querySelectorAll('.button-group a.btn')
for (const item of buttons) {
// Define what happens when they are clicked
item.addEventListener('click', (evt) => {
// First, grab its data-filter attribute
const value = item.getAttribute("data-filter")
// Update its visibility.
// If it was visible, hide it. If it was hidden, show it.
updateVisibilityFor(value)
// If the value to filter on was included in the list of values
if (filterValues.includes(value)) {
// Then we remove it from the values to filter on
filterValues.splice( filterValues.indexOf(value), 1 )
// And we toggle the active and inactive classes
item.classList.toggle('active');
item.classList.toggle('inactive');
} else {
// Otherwise, we add it back into the list of values to filter on
filterValues.push(value)
// And we toggle the active and inactive classes
item.classList.toggle('active');
item.classList.toggle('inactive');
}
// After all work is done, check if the grid has become empty.
checkEmptyGrid()
})
}
There are two helper functions. First, a helper function to update visibility of items. Notice that the setTimeout()
is a must, and that its duration must be identical to the CSS animation duration. This is because of a peculiarity when animating display properties.
function updateVisibilityFor(value) {
// Select all DOM elements to toggle visibility for.
const toggleMe = document.querySelectorAll(`.grid > div.${value}`)
toggleMe.forEach((item) => {
// If it is currently shown, then:
if (item.classList.contains('post-shown')) {
// SET A TIMEOUT to hide it.
// Note: The duration must be identical to the CSS animation duration
setTimeout(() => {item.style.display = 'none'}, 200)
}
// If it is hidden, then:
if (item.classList.contains('post-hidden')) {
// show it (immediately).
item.style.display = 'block'
}
// Always toggle the two classes indicating if it is shown or hidden
item.classList.toggle('post-shown')
item.classList.toggle('post-hidden')
})
}
The second helper function is relatively simple: it checks if all items are hidden, and if so, it shows the piece of text indicated by emptygrid
.
function checkEmptyGrid() {
// Grab all items in the grid
const items = document.querySelectorAll(`.grid > div`)
// Grab the piece of text you see when the grid is empty
const text = document.querySelector('#emptygrid')
// Loop through all items in the grid
let found = false;
for (const item of items) {
// If at least one of them is shown
if (item.classList.contains('post-shown')) {
// Then we don't have to do anything
found = true
break
}
}
// Set the display style of the text accordingly
text.style.display = !found ? 'unset' : 'none'
}
There are CSS animations on hiding and showing the items. This is achieved using following classes (CSS truncated to only relevant items). Note that the duration of all the animations are 200 ms
– this must be identical to the delay in setTimeout()
when toggling visibility.
.post-item {
transition: all .2s ease;
}
.post-hidden {
animation: hide .2s;
animation-fill-mode: forwards;
display: block;
}
.post-shown {
animation: show .2s;
display: block;
}
The related keyframes are shown below. Note that for the hide
animation there is a keyframe at 99% - this is again because of the peculiarity when animating display properties.
@keyframes show {
0% {
opacity: 0;
transform: scale(0);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes hide {
0% {
display: block;
opacity: 1;
transform: scale(1);
}
99% {
display: block;
opacity: 0;
transform: scale(0);
}
100% {
display: none;
opacity: 0;
transform: scale(0);
}
}
The source code is available on my github, in case you want to see.