This tutorial provides the PHP & JS codes that can be pasted in order to filter your markers on a map based on terms of a taxonomy inside the Bricks Builder.
Table of Contents
Requirements
- All the codes running from PART 1, PART 2 and PART 3 of the map series.
- Create a custom taxonomy (in our example we used
store_categories
) and assign each custom post a specific term (in our example we usedmobile
andinternet
).
Bricks structure
Since we are willing to filter the markers based on a term filter, the first thing to do is to create a list of buttons with each term name:
To achieve it, we created a button container just after the Input Search code element that contains one static button – the “ALL” button that will erase the filters – and a query loop div that will loop through the terms and print a button for each of them.
On the Button wrapper, make sure to add the filterbtn-wrapper
class:
On the Button “ALL” we’ll add two classes: .filterbtn
and .filterbtn--active
:
The .filterbtn
class is shared with all the buttons – including the term ones. The .filterbtn--active
is here for two reasons:
- it makes the “ALL” button active by default on load
- it consents us to style the active state as we want, inside the builder.
Let’s also change the link type to External URL and set #
as value:
And add a data-attribute called data-filter
with value of *
:
Now let’s jump on the Terms Query loop and fill the following setting:
Let’s move on to our last new element: the button inside our query loop. The text will be dynamically generated by the term_name function. We’ll also add the filterbtn
class and set the external link as #
:
And add a data-filter
attribute with the term_name
function as a value:
Now that our filter buttons are all set, we need to add the filter condition for each custom post. To do that let’s jump back inside our Query Loop Stores element (the one below the isotope wrapper):
And let’s add a data-filter
attribute as well with a custom function (that we’ll create later inside our functions.php file) that will output all the terms used in each custom post:
We’re all set, let’s code some cool stuff!
The PHP
The code from the previous tutorial doesn’t require any change, but let’s just add the custom function we used earlier to output all terms of a custom post as a list of strings. Copy/paste the following code inside your functions.php file:
function bl_get_terms_from_post() {
$term_obj_list = get_the_terms( $post->ID, 'store_categories' );
$terms_string = join( ', ', wp_list_pluck( $term_obj_list, 'name' ) );
return $terms_string;
}
Note that store_categories
is the name of our taxonomy. Make sure to match your own taxonomy’s name.
The JavaScript
First I’ll paste the full code that you need to replace, then I’ll comment on the important changes. Here is the final code:
window.addEventListener('load', () => {
const canvas = document.querySelector('#map');
if (!document.body.classList.contains('bricks-is-frontend') || canvas === null) {
return;
}
var mbAttr = 'Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>';
var mbUrl = 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw';
var streets = L.tileLayer(mbUrl, {
id: 'mapbox/streets-v11',
tileSize: 512,
zoomOffset: -1,
attribution: mbAttr
});
var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
});
var satellite = L.tileLayer(mbUrl, {
id: 'mapbox/satellite-v9',
tileSize: 512,
zoomOffset: -1,
attribution: mbAttr
});
var map = L.map('map', {
center: [canvas.dataset.leafletLat, canvas.dataset.leafletLong],
zoom: canvas.dataset.leafletZoom,
maxZoom: canvas.dataset.leafletMaxZoom,
layers: [osm, cities]
});
var baseLayers = {
'OpenStreetMap': osm,
'Streets': streets,
'Satellite': satellite
};
var layerControl = L.control.layers(baseLayers).addTo(map);
// Jump to map on click
const storeHeadersLink = document.querySelectorAll('.store-header-link');
if (storeHeadersLink.length > 0) {
storeHeadersLink.forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
let lat = e.target.dataset.leafletLat;
let long = e.target.dataset.leafletLong;
map.flyTo([lat, long], 17);
});
});
};
// Filter search input with IsotopeJS
// quick search regex
var qsRegex;
var filterSelector = "*";
// init Isotope
var iso = new Isotope('.isotope-wrapper', {
itemSelector: '.store-wrapper',
layoutMode: 'fitRows',
filter: function(itemElem) {
var search = qsRegex ? itemElem.textContent.match(qsRegex) : true;
var filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true;
return search && filterRes;
},
});
var mapRendering = () => {
boundsList = [];
var filteredItems = iso.filteredItems;
if (filteredItems < 1) {
return;
}
filteredItems.forEach(elm => {
elmt = elm.element;
let header = elmt.querySelector('.store-header')
let storeId = header.dataset.store;
let lat = header.dataset.leafletLat;
let long = header.dataset.leafletLong;
stores[storeId].addTo(cities);
boundsList.push(new L.LatLng(lat, long));
});
map.fitBounds(boundsList, {
padding: [50, 50]
});
}
// use value of search field to filter
var quicksearch = document.querySelector('.quicksearch');
quicksearch.addEventListener('keyup', debounce(function() {
qsRegex = new RegExp(quicksearch.value, 'gi');
// delete the current markers
cities.clearLayers();
//filter the store list
iso.arrange();
//add the new filtered markers to the map
mapRendering();
}, 200));
// debounce so filtering doesn't happen every millisecond
function debounce(fn, threshold) {
var timeout;
threshold = threshold || 200;
return function debounced() {
clearTimeout(timeout);
var args = arguments;
var _this = this;
function delayed() {
fn.apply(_this, args);
}
timeout = setTimeout(delayed, threshold);
};
};
// Filter terms
// bind filter button click
var filtersElem = document.querySelectorAll(".filterbtn-wrapper .filterbtn");
if (filtersElem.length > 0) {
filtersElem.forEach(elem => elem.addEventListener("click", function(event) {
event.preventDefault();
var filterValue = event.target.getAttribute("data-filter");
filterSelector = filterValue;
// delete the current markers
cities.clearLayers();
//filter the store list
iso.arrange();
//add the new filtered markers to the map
mapRendering();
}));
};
// change filterbtn--active class on buttons
for (var i = 0, len = filtersElem.length; i < len; i++) {
var buttonGroup = filtersElem[i];
radioButtonGroup(buttonGroup);
}
function radioButtonGroup(buttonGroup) {
buttonGroup.addEventListener("click", function(event) {
filtersElem.forEach(btn => btn.classList.remove("filterbtn--active"));
event.target.classList.add("filterbtn--active");
});
}
});
The first change we’ve made is to add a second filter method to our Isotope initialization:
// init Isotope
var iso = new Isotope('.isotope-wrapper', {
itemSelector: '.store-wrapper',
layoutMode: 'fitRows',
filter: function(itemElem) {
var search = qsRegex ? itemElem.textContent.match(qsRegex) : true;
var filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true;
return search && filterRes;
},
});
Note that we return the result of both the search filter AND (not OR) the buttons filters. That means you the user will be able to use both types of filters simultaneously.
The second important change is we decided to move all the code dedicated to the filter process of the markers into a separate function, so we can call that function mapRendering()
whether the filter is the search input or buttons:
// Populate the filtered markers
var mapRendering = () => {
boundsList = [];
var filteredItems = iso.filteredItems;
if (filteredItems < 1){
return;
}
filteredItems.forEach(elm => {
elmt = elm.element;
let header = elmt.querySelector('.store-header')
let storeId = header.dataset.store;
let lat = header.dataset.leafletLat;
let long = header.dataset.leafletLong;
stores[storeId].addTo(cities);
boundsList.push(new L.LatLng(lat, long) );
} );
map.fitBounds(boundsList, {padding: [50, 50]});
}
We added a eventListener
on the filter buttons so on each button’s click, our script will retrieve the term associated with it and apply the filters both on the list and the map:
// bind filter button click
var filtersElem = document.querySelectorAll(".filterbtn-wrapper .filterbtn");
if (filtersElem.length > 0) {
filtersElem.forEach ( elem => elem.addEventListener("click", function(event) {
event.preventDefault();
var filterValue = event.target.getAttribute("data-filter");
filterSelector = filterValue;
// delete the current markers
cities.clearLayers();
//filter the store list
iso.arrange();
//add the new filtered markers to the map
mapRendering();
}));
};
Finally, the last part of the code is dedicated to the active class .filterbtn--active
added when a filter button is clicked (only for styling purposes):
// change filterbtn--active class on buttons
for (var i = 0, len = filtersElem.length; i < len; i++) {
var buttonGroup = filtersElem[i];
radioButtonGroup(buttonGroup);
}
function radioButtonGroup(buttonGroup) {
buttonGroup.addEventListener("click", function(event) {
filtersElem.forEach(btn => btn.classList.remove("filterbtn--active"));
event.target.classList.add("filterbtn--active");
});
}
And that’s it!
Conclusion
Hopefully, everything is in place and you should be able to test the new term filters on frontend:
That’s it for now! I hope you enjoyed this tutorial and let’s catch up on the next one!
2 comments
Paulo
Great as always Maxime. You think it's possible to add geolocation to these tutorials?. For example filtering and ordering by proximity and "search as I move" just like WPBG
https://demos.wpgridbuilder.com/map/
I love the way when clicking in the listing item, it shows the location in the map
Thank you!
Maxime Beguin
Hey Paulo,
Yes, it’s definitely possible! Here is a nice article about leaflet.js and geolocation: https://www.section.io/engineering-education/how-to-build-a-real-time-location-tracker-using-leaflet-js/