Continuing work on my project in Flask and Bulma, I needed a neat way to display a photo gallery. There are plenty of premade gallery setups that I could have used, but I wanted to make something using Bulma, Flask, HTMX, and Hyperscript as much as possible. This led to a lot of frustration trying to make Bulma do something it wasn't quite capable of doing.
I wanted to display pictures in a neat organized manner where it would be easy to see a whole bunch of them at once, in thumbnail form, and then to show them in full screen when they are clicked. Most places around the net have this solved by using some kind of modal. Bulma does have the modal capability, which I intended to use for this purpose. However, the way Bulma is designed to show images is to make them conform to some preset aspect ratios regardless of their actual proportions. After trying everything I could think of, I ended up adding some of my own CSS to show these full-screen images properly.
Thumbnail gallery
To make a thumbnail gallery, I created square thumbnails out of all the uploaded images. Then I displayed the thumbnails using these templates:
In user_pics.html
:
<div id="pics" class="columns is-multiline">
<!-- Show all the pics -->
{% for pic in pics.items %}
{% include 'photo.html' %}
{% endfor %}
</div>
This creates a row of columns and the class .is-multiline
makes it so that if any columns don't fit in the first row, they will continue in the next row. It's pretty neat.
Then in photo.html
I defined the columns in such a way that each photo was in a column by itself. I've set the size of the thumbnail by using the column width modifier classes .is-one-quarter-desktop
and .is-half-tablet
.
<div class="column is-one-quarter-desktop is-half-tablet">
<div class="card">
<div class="card-image">
<a hx-get="{{ url_for('users.img_show', img_id=pic.id, username=user.username, page=page) }}"
hx-trigger="click"
hx-target="#modal"
hx-swap="innerHTML">
<figure class="image mx-0 px-0">
<img src="{{ url_for('static', filename='imgs/' + pic.thumbnail) }}" alt="">
</figure>
</a>
</div>
</div>
</div>
I still haven't decided if I want to show some text with each picture or not, but that is why I used .card
and .card-image
classes. If I change my mind later I can easily add a .card-content
<div/>
to show a caption under each thumbnail.
Calling the modal
As you can see above, there is some HTMX in the link for each picture which is used to open the modal and fetch the slideshow gallery. To give a target to this call, I added the following at the end of user_pics.html
:
<!-- Modal -->
<div id="modal"></div>
The HTMX calls the users.img_show
function in routes.py
for users Blueprint. This is what this function looks like:
@users.route("/user/<string:username>/img/<int:img_id>", methods=['GET'])
def img_show(img_id, username):
page = request.args.get('page', 1, type=int)
user = User.query.filter_by(username=username).first_or_404()
pics = Picture.query.filter_by(author=user)\
.order_by(Picture.date_posted.desc())\
.paginate(page=page, per_page=PICS_PER_PAGE)
if pics:
return render_template('modal_img_show.html', pics=pics, img_id=img_id)
else:
return "No pic found."
I use the user and page information to load all of the images visible on the current page, then render and return modal_img_show.html
template with the ID of the selected image. This is where the whole slideshow gallery resides.
<div id="modal_img_show" class="modal is-fullwidth is-active">
<div class="modal-background cancel" _="on click toggle .is-active on #modal_img_show"></div>
<div id="img_show" class="modal-content img_show_modal is-vcentered">
{% if pics.total > 0 %}
{% for pic in pics.items %}
<img class="img_show {{ 'is-active' if pic.id==img_id }}" src="{{ url_for('static', filename='imgs/' + pic.file_name) }}">
{% endfor %}
<!-- Next and previous buttons -->
<a class="prev"
_="on click
set currImg to previous <img.is-active/> then
toggle .is-active on currImg then
set prevImg to previous <img.img_show/> from currImg with wrapping then
toggle .is-active on prevImg then
end">❮</a>
<a class="next"
_="on click
set currImg to previous <img.is-active/> then
toggle .is-active on currImg then
set nextImg to next <img.img_show/> from currImg with wrapping then
toggle .is-active on nextImg then
end">❯</a>
{% endif %}
</div>
<button class="modal-close is-large" aria-label="close" _="on click toggle .is-active on #modal_img_show"></button>
</div>
Let's examine the interesting parts of this code.
Setting up the modal
Since this is a Bulma modal, the first line takes care of setting it up as such. It also sets it to .is-active
so it can be visible as soon as it loads.
The second line defines the background of the modal, which is dark but slightly transparent. I've added some Hyperscript to it to close the modal if the background is clicked:
<div class="modal-background cancel" _="on click toggle .is-active on #modal_img_show"></div>
For this application, Hyperscript was very simple and intuitive. I added the same code to the close button at the end of the modal (which shows up in the top right corner).
<button class="modal-close is-large" aria-label="close" _="on click toggle .is-active on #modal_img_show"></button>
With the surrounding parts of the modal defined, it was time to focus on .modal-content
where the fun stuff happens. Before doing anything inside it, I had to define some CSS to stretch the content across the full screen. Well, 95% of it.
.img_show_modal {
height: 95%;
overflow: hidden;
}
Slideshow gallery
With the container showing up properly, I needed some way to show the images. I used W3 Schools' example of a slideshow gallery as a starting point. I didn't need the thumbnails shown under the current picture, so I omitted that part.
I also omitted most of the styling factors for the gallery, leaving just the <img/>
tags. I iterated through the images and loaded them all up, but only the current image was set to .is-active
.
{% for pic in pics.items %}
<img class="img_show {{ 'is-active' if pic.id==img_id }}" src="{{ url_for('static', filename='imgs/' + pic.file_name) }}">
{% endfor %}
Here I again defined some CSS to make the picture cover the available area without distorting its original aspect ratio and to define that .is-active
means to display the image.
.img_show {
display: none;
-o-object-fit: contain;
object-fit: contain;
height: 100%;
width: 100%;
}
.img_show.is-active {
display: block;
}
The last part was attaching some Hyperscript to the next and previous buttons to move through the slides.
<!-- Next and previous buttons -->
<a class="prev"
_="on click
set currImg to previous <img.is-active/> then
toggle .is-active on currImg then
set prevImg to previous <img.img_show/> from currImg with wrapping then
toggle .is-active on prevImg then
end">❮</a>
<a class="next"
_="on click
set currImg to previous <img.is-active/> then
toggle .is-active on currImg then
set nextImg to next <img.img_show/> from currImg with wrapping then
toggle .is-active on nextImg then
end">❯</a>
This part was a little tricky. Most of the Hyperscript documentation is written with the assumption that the actions are being done around the object that was listening for an event. In this case, the button is listening, and then the actions need to happen to the active image and images around it.
Once I figured out that I could put the currently active image into a variable, I was able to find the images next to it. The steps were then intuitive:
Find the currently active image with
previous <img.is-active/>
which works because the links to previous/next buttons are right under all the <img/> tags in the DOM, so the previous<img/>
that has.is-active
is the one we're looking forPut it into
currImg
variableRemove
.is-active
class from it to hide itFind the previous/next image to the current one with
previous <img.img_show/> from currImg with wrapping
ornext <img.img_show/> from currImg with wrapping
Put them into appropriately named variables
prevImg
ornextImg
Add
.is-active
class to make them visible
And there you have it. It was a fun little project that let me explore a few more features of Hyperscript.