I want to use ShuffleJS (see https://vestride.github.io/Shuffle) to filter out items on a <select>
list by typing my search keyword on the <select>
‘s input text area and the items in the <select>
dropdown to be filtered out dynamically as I type. Unfortunately I have not managed this. And so, I followed the idea in How to add shufflejs for select tag which suggests creating a DIY <select>
of some sorts which then ShuffleJS seems to handle OK.
So, instead of using a <select>
:
- I create a
<textarea>
to type my search. It will act the same as the textarea of a<select>
, - I stored all items (to be filtered out) as
<li>
elements inside a<ul>
, - I let ShuffleJS know where these are located.
I have one complication which I think I handled well:
My option items form a very long list and I have two or more of those search-and-select elements in my form with the exact same option items. For example they can be From/To locations in a map, train stops, etc. And so I decided to use only 1 list of option items (the train stops) to be shared among many From/To/etc. search text areas in the same page/form.
This works mostly OK but there are a few problems that I need help with:
-
The most important problem is with the logic of closing the “dropdown” containing the items to be filtered/selected when the user has made a selection by clicking on an option item in the “dropdown” or user has not made a selection by clicking elsewhere in the form. (By “dropdown” I mean the thing that pops up when I click on the search text area and contains, initially, all the option items, later to be filtered out as I type in that area.). I detect that the user has clicked on somewhere outside the search text area by the
onblur
event. In which case I close the “dropdown” and clear the search area with no selection by the user. This works fine. However, when the user clicks on an item on the “dropdown” with the intention to select an option item, this is still considered anonblur
event for the search textarea and it is handled first, i.e. no selection, text area is cleared. By the time this is handled, there is no “dropdown” and theonclick
on the option item fails. I solved this by delaying theonblur
for a few milliseconds hoping that theonclick
will be handled first, save the user selection and then close the “dropdown”. This works but I suspect there is a race condition looming there and I am not comfortable with this. Is there any better solution? -
I would like to make the options dropdown appear not only when I click on the search textarea but also when I use the keyboard to navigate the elements, using the TAB key. the
onfocus
event does not seem to work. -
When I click on a search text area and the “dropdown” appears with all the options, I can not just click on one and select it if I don’t first type something in the text area in order for ShuffleJS to do some filtering, even if I erase my typing afterwards (so as to obtain the unfiltered list of option items). In this situation (i.e. without first typing a search term), clicking on an option does not copy it onto the text area and closing the “dropdown”. Why is that and do you have any hints on how I can fix it?
I don’t use any framework. Just javascript and ShuffleJS (perhaps bootstrap/jquery).
Edit: I have a fiddle for this here : https://jsfiddle.net/bliako/8sm13gx9/11/
The code is below:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>ShuffleJS select dropdown example</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/w3-css/4.1.0/w3.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/Shuffle/6.1.0/shuffle.min.js" integrity="sha512-r8mIpk3ypCMwNxH6srRZGbjHQlOFt3Mq2vrZ/iymx6g9JUqTeVavofeX6gbrxAC74X5HmBy5gxyhCm6OiXrM0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</body>
<div class="row h-100 justify-content-center align-items-center">
<form
id="searchform"
name="searchform"
class="myappsearch"
action=''
>
<div class="form-group col"> <!-- begin the A search text area -->
<legend><span>Select A <small>(onblur is handled with a timeout after onclick, works)</small>:</span></legend>
<textarea class="form-control form-control-sm filter__search js-shuffle-search"
id="search-textarea-A"
type="search"
rows="1"
required
onkeydown='handle_keydown_in_search_textarea(this, event);'
onblur='this.value = ""; mythis = this; setTimeout(function() { close_shuffle_dropdown(mythis); }, 200);'
onclick='A_clicked(this);'
></textarea>
<!-- this is a lame solution to the dropdown disappearing before the
onclick event on it is handled: Just act on the onblur after some
milliseconds so that the onclick has time to be handled first:
onblur='close_shuffle_dropdown(this);'
onblur='this.value = ""; mythis = this; setTimeout(function() { close_shuffle_dropdown(mythis); }, 200);'
-->
</div> <!-- end the A search text area: -->
<br />
<div class="form-group col"> <!-- begin the Select B search text area -->
<legend><span>Select B <small>(onblur is handled before onclick, problem)</small>:</span></legend>
<textarea class="form-control form-control-sm filter__search js-shuffle-search"
id="search-textarea-B"
type="search"
rows="1"
required
onkeydown='handle_keydown_in_search_textarea(this, event);'
onblur='close_shuffle_dropdown(this);'
onclick='B_clicked(this);'
></textarea>
</div> <!-- end the Select B search text area -->
</div>
<!-- this UL contains all the available options to select
Shufflejs displays this in its own way in a "dropdown"
and then, by typing on ANY of the two search textareas
you can see the options filtered out.
Clicking on an option will copy the value of
that option to the respective search textarea (we have two!)
-->
<div id='select-options-container'
class='custom-select'
style='display:none; position:absolute;'
>
<ul id='select-options'
style='background:#000000;width:100px;'
>
<li class='select-item'
style='color:#ffffff;'
id='Orange'
onclick='item_has_been_selected(this);'
>Orange</li>
<li class='select-item'
style='color:#ffffff;'
id='Black'
onclick='item_has_been_selected(this);'
>Black</li>
<li class='select-item'
style='color:#ffffff;'
id='Green123'
onclick='item_has_been_selected(this);'
>Green123</li>
<li class='select-item'
style='color:#ffffff;'
id='Green567'
onclick='item_has_been_selected(this);'
>Green567</li>
</ul>
</div>
<script>
var Shuffle = window.Shuffle;
var currentSearchTextarea = null;
class ShufflejsFilterer {
constructor(element) {
this.element = element;
this.shuffle = new Shuffle(element, {
itemSelector: '.select-item',
//sizer: element.querySelector('.my-sizer-element'),
});
this._activeFilters = [];
// Log events:
//this.addShuffleEventListeners();
// create the search filter:
this.addSearchFilter();
}
/**
* Shuffle uses the CustomEvent constructor to dispatch events. You can listen
* for them like you normally would (with jQuery for example).
*/
addShuffleEventListeners() {
this.shuffle.on(Shuffle.EventType.LAYOUT, (data) => {
console.log('layout. data:', data);
});
this.shuffle.on(Shuffle.EventType.REMOVED, (data) => {
console.log('removed. data:', data);
});
}
// Advanced filtering
addSearchFilter() {
// modified to handle more than 1 search-boxes
const searchInputs = document.querySelectorAll('.js-shuffle-search');
if (searchInputs.length==0) {
return;
}
// no we call this on our
// searchInputs.forEach( (asearchInput) => {
// asearchInput.addEventListener('keydown', this._handleSearchKeydown.bind(this));
// });
}
/**
* Filter the shuffle instance by items with a title that matches the search input.
* @param {Event} evt Event object.
*/
_handleSearchKeydown(evt) {
// this is e.g. the Search textarea A or B
currentSearchTextarea = evt.target;
// and this is what the user has typed as a search text
const searchText = evt.target.value.toLowerCase();
console.log("searching with this "+searchText);
this.shuffle.filter((element, shuffle) => {
// get the text content of the client element which is
// the text (or anything else via data-attributes)
// of each element part of the select-items set
const contents = element.textContent.toLowerCase().trim();
// and filter it out if it does not contain our search text
return contents.indexOf(searchText) !== -1;
});
}
} // end class ShufflejsFilterer
///////////////////////////////////////
/// other JS functions
///////////////////////////////////////
function handle_keydown_in_search_textarea(
caller_obj,
event
){
if (event.keyCode == 27){
// on ESCape close the shuffle-dropdown and select nothing
caller_obj.value = '';
close_shuffle_dropdown(caller_obj);
return; // nothing to do
}
// let ShuffleJS handle it now
window.myFilterer._handleSearchKeydown(event);
}
function close_shuffle_dropdown(
caller_obj
){
console.log("close_shuffle_dropdown() : called ...");
var selObj = document.getElementById('select-options-container');
//selObj.style.position = 'absolute'; // already set
selObj.style.display = 'none'; // hide
// we need to reset the contents of the <ul>
// if you don't complete (by clicking an item of the <ul>)
// the <ul> appears truncated but search text is empty.
// this is what causes it to reset!!!! it is part of _init()
window.myFilterer.shuffle.filter(window.myFilterer.shuffle.options.group, window.myFilterer.shuffle.options.initialSort);
}
function item_has_been_selected(
item_obj
){
// once an item has been selected we copy its value to the currently
// selected textarea:
currentSearchTextarea.value = item_obj.textContent;
// and then we close the shuffle dropdown:
document.getElementById("select-options-container").style.display = "none";
}
// Search textarea (A) was clicked, so we show the shuffle dropdown
function A_clicked(
obj
){
const pos = obj.getBoundingClientRect();
var selObj = document.getElementById('select-options-container');
/* place the "dropdown" with all the select items in there,
as formatted by Shufflejs, under the client element
(search textarea)
*/
//selObj.style.position = 'absolute'; // already set
selObj.style.left = pos.left+'px';
selObj.style.top = String(pos.bottom+10)+'px';
// and show the "dropdown"
selObj.style.display = 'inline';
}
// Search textarea (B) was clicked, so we show the shuffle dropdown
// it is the same as the above
function B_clicked(
obj
){
console.log("my val: "+obj.value);
A_clicked(obj);
}
///////////////////////////////////////
/// things to do when DOM loaded:
///////////////////////////////////////
document.addEventListener('DOMContentLoaded', () => {
// create the Shufflejs filterer
window.myFilterer = new ShufflejsFilterer(document.getElementById('select-options'));
/*
window.myFilterer.applyCss({
INITIAL: {
position: 'absolute',
top: 0,
visibility: 'visible',
willChange: 'transform',
}
});
*/
['search-textarea-A', 'search-textarea-B'].forEach( (k) => {
var obj = document.getElementById(k);
obj.value = '';
});
}); // end DOMContentLoaded
</script>
</body>
</html>