I’m using Bulma.io for a while now. it’s a beautiful CSS framework. My latest project Xlog uses Bulma. This project includes a tools dialog, using Bulma Modal. That solution needed a lot of javascript behavior that should be possible with an HTML5 dialog tag. The following post describes how I simplified the solution and reduced the Javascript involved.
Copy pasta from Bulma Modal documentation got me something running that looks like this.
The code that powers it is a piece of HTML styles with Bulma classes
1<button class="button js-modal-trigger" data-target="modal-js">
2 Tools: Ctrl+K
3</button>
4
5<div id="modal-js" class="modal" tabindex="0">
6 <div class="modal-background"></div>
7
8 <div class="modal-content">
9 <div class="box">
10 <aside class="menu">
11 // Tools HTML here
12 </aside>
13 </div>
14 </div>
15
16 <button class="modal-close is-large" aria-label="close"></button>
17</div>
And some javascript that show/hides the dialog
1 (function() {
2 // Functions to open and close a modal
3 function openModal($el) {
4 $el.classList.add('is-active');
5 }
6
7 function closeModal($el) {
8 $el.classList.remove('is-active');
9 }
10
11 function closeAllModals() {
12 (document.querySelectorAll('.modal') || []).forEach(function($modal) {
13 closeModal($modal);
14 });
15 }
16
17 // Add a click event on buttons to open a specific modal
18 (document.querySelectorAll('.js-modal-trigger') || []).forEach(function($trigger) {
19 const modal = $trigger.dataset.target;
20 const $target = document.getElementById(modal);
21
22 $trigger.addEventListener('click', function() {
23 openModal($target);
24 });
25 });
26
27 // Add a click event on various child elements to close the parent modal
28 (document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(function($close) {
29 const $target = $close.closest('.modal');
30
31 $close.addEventListener('click', function() {
32 closeModal($target);
33 });
34 });
35
36 // Add a keyboard event to close all modals
37 document.addEventListener('keydown', function(event) {
38 const e = event || window.event;
39 const kCharCode = "K".charCodeAt();
40
41 if (e.keyCode === 27) { // Escape key
42 closeAllModals();
43 return;
44 }
45
46 if ( e.keyCode === kCharCode && ( e.metaKey || e.ctrlKey ) ) {
47 e.preventDefault();
48 const $target = document.getElementById('modal-js');
49 openModal($target);
50 $target.focus();
51 }
52
53 });
54 })();
I don’t know about you but this solution looks too complicated. there are lot of Javascript at play here that we probably don’t need it.
I tried to have an input to filter the tools at the top of the dialog and have it focused when the dialog appears. but that required more javascript. I know HTML5 has autofocus
attribute that works with dialog
. When the dialog is shown it will autofocus the input by default. so apparently I should turn around and use dialog
tag and autofocus
attribute.
HTML5 has a dialog tag with some default behavior. So instead of using a Div tag I replaced it with dialog
.
That means I can show and hide the dialog with .showModal
and .close
methods instead adding/removing is-active
class.
When I did that the dialog didn’t appear. so turns out Bulma modal
class hides the dialog. The browser already does that. so no need to use Bulma modal
class.
To hide a dialog HTML offers another synergy with form
tag. adding a form tag with method="dialog"
and a button to submit it will hide it’s parent dialog.
1<dialog id="tools-modal">
2 <div class="modal-content">
3 <div class="box">
4 <aside class="menu">
5 // TOOLS HTML HERE
6 </aside>
7 </div>
8 </div>
9
10 <form method="dialog">
11 <button class="modal-close is-large" aria-label="close"></button>
12 </form>
13</dialog>
That changes the dialog look
I noticed that focus is set on the first item in the list. but this list will be longer so I need to have a search input at the top that gets the focus automatically and filter the following list.
Adding the following before the aside
tag does the trick.
1 <div class="field">
2 <div class="control">
3 <input class="input" type="search" placeholder="Search..." autofocus/>
4 </div>
5 </div>
Showing the dialog focuses the input instead of the link:
There is so many in the original JS code to allow for multiple dialogs and hide the dialog when clicking different parts of the page when the dialog is open. I have one dialog here so lets clean it up.
1 const tools = document.getElementById('tools-modal');
2
3 document.addEventListener('keydown', function(e) {
4 const kCharCode = "K".charCodeAt();
5
6 if (e.keyCode === 27) { // Escape key
7 tools.close();
8 return;
9 }
10
11 if ( e.keyCode === kCharCode && ( e.metaKey || e.ctrlKey ) ) {
12 e.preventDefault();
13 tools.showModal();
14 }
15 });
The button that shows the dialog was wired with the openModal
function dynamically. there is also no need for that we can call window.openToolsModal
in onclick
attribute
1<button class="button" onclick="tools.showModal()">Tools: Ctrl+K</button>
I need to filter the list of items that shows under the input whenever the user writes a keyword. tools items are in li
tags. so I created a function that takes a keyword and hide all li
tags in the dialog that doesn’t include this keyword.
1 function filterToolsList(v) {
2 const keyword = v.toLowerCase();
3 const lis = document.querySelectorAll("#tools-modal li");
4
5 for(var i=0; i < lis.length; i++){
6 const li = lis[i];
7 if( li.textContent.toLowerCase().includes(keyword) ) {
8 li.classList.remove("is-hidden");
9 } else {
10 li.classList.add("is-hidden");
11 }
12 }
13 }
Then called it onkeyup
of the search input.
1<input class="input" type="search" placeholder="Search..." onkeyup="filterToolsList(this.value)" autofocus/>
And to save myself some code I passed the input value directly as I don’t need the keyup event nor the whole input element. I just needed the value of the input in this case so I passed exactly what I need.
The full implementation can be found on xlog repository on github. I tried to remove irrelevant HTML attributes and surrounding code in this post for simplification.