Skip to content

Commit

Permalink
"Agnostic" filters - decouple filters from datatables (#5714)
Browse files Browse the repository at this point in the history
* wip

* wip

* Apply fixes from StyleCI

[ci skip] [skip ci]

* wip

* Apply fixes from StyleCI

[ci skip] [skip ci]

* remove filters navbar component

---------

Co-authored-by: StyleCI Bot <bot@styleci.io>
  • Loading branch information
pxpm and StyleCIBot authored Feb 7, 2025
1 parent 5f6fbef commit 2c9bea6
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 109 deletions.
25 changes: 23 additions & 2 deletions src/resources/views/crud/inc/datatables_logic.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,14 +288,35 @@ functionsToRunOnDataTablesDrawEvent: [],
"<'table-footer row mt-2 d-print-none align-items-center '<'col-sm-12 col-md-4'l><'col-sm-0 col-md-4 text-center'B><'col-sm-12 col-md-4 'p>>",
}
}
</script>
</script>
@include('crud::inc.export_buttons')

<script type="text/javascript">
// TODO: this needs to be agnostic per filter navbar as in the future hopefully we can have more than one
// table in the same page and setup filters for each one.
document.addEventListener('backpack:filters:cleared', function (event) {
// behaviour for ajax table
var new_url = '{{ url($crud->getOperationSetting("datatablesUrl").'/search') }}';
var ajax_table = new DataTable('#crudTable');
// replace the datatables ajax url with new_url and reload it
ajax_table.ajax.url(new_url).load();
// remove filters from URL
crud.updateUrl(new_url);
});
document.addEventListener('backpack:filter:changed', function (event) {
let filterName = event.detail.filterName;
let filterValue = event.detail.filterValue;
let shouldUpdateUrl = event.detail.shouldUpdateUrl;
let debounce = event.detail.debounce;
updateDatatablesOnFilterChange(filterName, filterValue, filterValue || shouldUpdateUrl, debounce);
});
jQuery(document).ready(function($) {
window.crud.table = $("#crudTable").DataTable(window.crud.dataTableConfiguration);
window.crud.updateUrl(location.href);
// move search bar
Expand Down
267 changes: 160 additions & 107 deletions src/resources/views/crud/inc/filters_navbar.blade.php
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
<nav class="navbar navbar-expand-lg navbar-filters mb-0 py-0 shadow-none">
{{-- Brand and toggle get grouped for better mobile display --}}
<a class="nav-item d-none d-lg-block my-auto"><span class="la la-filter"></span></a>
<button class="navbar-toggler ms-3"
type="button"
data-toggle="collapse" {{-- for Bootstrap v4 --}}
data-target="#bp-filters-navbar" {{-- for Bootstrap v4 --}}
data-bs-toggle="collapse" {{-- for Bootstrap v5 --}}
data-bs-target="#bp-filters-navbar" {{-- for Bootstrap v5 --}}
aria-controls="bp-filters-navbar"
aria-expanded="false"
aria-label="{{ trans('backpack::crud.toggle_filters') }}">
{{-- Brand and toggle get grouped for better mobile display --}}
<a class="nav-item d-none d-lg-block my-auto"><span class="la la-filter"></span></a>
<button class="navbar-toggler ms-3"
type="button"
data-toggle="collapse" {{-- for Bootstrap v4 --}}
data-target="#bp-filters-navbar" {{-- for Bootstrap v4 --}}
data-bs-toggle="collapse" {{-- for Bootstrap v5 --}}
data-bs-target="#bp-filters-navbar" {{-- for Bootstrap v5 --}}
aria-controls="bp-filters-navbar"
aria-expanded="false"
aria-label="{{ trans('backpack::crud.toggle_filters') }}">
<span class="la la-filter"></span> {{ trans('backpack::crud.filters') }}
</button>
</button>

{{-- Collect the nav links, forms, and other content for toggling --}}
<div class="collapse navbar-collapse" id="bp-filters-navbar">
{{-- Collect the nav links, forms, and other content for toggling --}}
<div class="collapse navbar-collapse" id="bp-filters-navbar">
<ul class="nav navbar-nav">
{{-- THE ACTUAL FILTERS --}}
@foreach ($crud->filters() as $filter)
@includeFirst($filter->getNamespacedViewWithFallbacks())
@endforeach
<li class="nav-item"><a href="#" id="remove_filters_button" class="nav-link {{ count(Request::input()) != 0 ? '' : 'invisible' }}"><i class="la la-eraser"></i> {{ trans('backpack::crud.remove_filters') }}</a></li>
{{-- THE ACTUAL FILTERS --}}
@foreach ($crud->filters() as $filter)
@includeFirst($filter->getNamespacedViewWithFallbacks())
@endforeach
<li class="nav-item"><a href="#" class="nav-link remove_filters_button {{ count(Request::input()) != 0 ? '' : 'invisible' }}"><i class="la la-eraser"></i> {{ trans('backpack::crud.remove_filters') }}</a></li>
</ul>
</div>{{-- /.navbar-collapse --}}
</nav>

@push('crud_list_scripts')
</div>{{-- /.navbar-collapse --}}
</nav>
@push('after_scripts')
@basset('https://unpkg.com/urijs@1.19.11/src/URI.min.js')
<script>
function addOrUpdateUriParameter(uri, parameter, value) {
var new_url = normalizeAmpersand(uri);
new_url = URI(new_url).normalizeQuery();
if(typeof addOrUpdateUriParameter !== 'function') {
function addOrUpdateUriParameter(uri, parameter, value) {
let new_url = URI(uri).normalizeQuery();
// this param is only needed in datatables persistent url redirector
// not when applying filters so we remove it.
Expand All @@ -40,106 +39,160 @@ function addOrUpdateUriParameter(uri, parameter, value) {
}
if (new_url.hasQuery(parameter)) {
new_url.removeQuery(parameter);
new_url.removeQuery(parameter);
}
if (value !== '' && value != null) {
new_url = new_url.addQuery(parameter, value);
new_url = new_url.addQuery(parameter, value);
}
$('#remove_filters_button').toggleClass('invisible', !new_url.query());
return new_url.toString();
}
function updateDatatablesOnFilterChange(filterName, filterValue, update_url = false, debounce = 500) {
// behaviour for ajax table
var current_url = crud.table.ajax.url();
var new_url = addOrUpdateUriParameter(current_url, filterName, filterValue);
new_url = normalizeAmpersand(new_url);
// add filter to URL
crud.updateUrl(new_url);
crud.table.ajax.url(new_url);
// when we are clearing ALL filters, we would not update the table url here, because this is done PER filter
// and we have a function that will do this update for us after all filters had been cleared.
if(update_url) {
// replace the datatables ajax url with new_url and reload it
callFunctionOnce(function() { refreshDatatablesOnFilterChange(new_url) }, debounce, 'refreshDatatablesOnFilterChange');
return new_url.normalizeQuery().toString();
}
}
if(typeof updatePageUrl !== 'function') {
function updatePageUrl(filterName, filterValue, currentUrl = null) {
currentUrl = currentUrl || window.location.href;
let newUrl = addOrUpdateUriParameter(currentUrl, filterName, filterValue);
crud.updateUrl(newUrl);
return newUrl;
}
}
if(typeof updateDatatablesOnFilterChange !== 'function') {
function updateDatatablesOnFilterChange(filterName, filterValue, update_url = false, debounce = 500) {
// behaviour for ajax tables
let new_url = updatePageUrl(filterName, filterValue, crud.table.ajax.url());
crud.table.ajax.url(new_url);
// when we are clearing ALL filters, we would not update the table url here, because this is done PER filter
// and we have a function that will do this update for us after all filters had been cleared.
if(update_url) {
// replace the datatables ajax url with new_url and reload it
callFunctionOnce(function() { refreshDatatablesOnFilterChange(new_url) }, debounce, 'refreshDatatablesOnFilterChange');
}
return new_url;
}
/**
* calls the function func once within the within time window.
* this is a debounce function which actually calls the func as
* opposed to returning a function that would call func.
*
* @param func the function to call
* @param within the time window in milliseconds, defaults to 300
* @param timerId an optional key, defaults to func
*
* FROM: https://stackoverflow.com/questions/27787768/debounce-function-in-jquery
*/
if(typeof callFunctionOnce !== 'function') {
return new_url;
}
}
/**
* calls the function func once within the within time window.
* this is a debounce function which actually calls the func as
* opposed to returning a function that would call func.
*
* @param func the function to call
* @param within the time window in milliseconds, defaults to 300
* @param timerId an optional key, defaults to func
*
* FROM: https://stackoverflow.com/questions/27787768/debounce-function-in-jquery
*/
if(typeof callFunctionOnce !== 'function') {
function callFunctionOnce(func, within = 300, timerId = null) {
window.callOnceTimers = window.callOnceTimers || {};
timerId = timerId || func;
if (window.callOnceTimers[timerId]) {
clearTimeout(window.callOnceTimers[timerId]);
}
window.callOnceTimers[timerId] = setTimeout(func, within);
window.callOnceTimers = window.callOnceTimers || {};
timerId = timerId || func;
if (window.callOnceTimers[timerId]) {
clearTimeout(window.callOnceTimers[timerId]);
}
window.callOnceTimers[timerId] = setTimeout(func, within);
}
}
function refreshDatatablesOnFilterChange(url)
{
// replace the datatables ajax url with new_url and reload it
crud.table.ajax.url(url).load();
}
}
if(typeof refreshDatatablesOnFilterChange !== 'function') {
function refreshDatatablesOnFilterChange(url)
{
// replace the datatables ajax url with new_url and reload it
crud.table.ajax.url(url).load();
}
}
function normalizeAmpersand(string) {
return string.replace(/&amp;/g, "&").replace(/amp%3B/g, "");
}
// button to remove all filters
document.addEventListener('DOMContentLoaded', function () {
// button to remove all filters
jQuery(document).ready(function($) {
$("#remove_filters_button").click(function(e) {
e.preventDefault();
// find all nav.navbar-filters
let filtersNavbar = document.querySelectorAll('.navbar-filters');
// behaviour for ajax table
var new_url = '{{ url($crud->getOperationSetting("datatablesUrl").'/search') }}';
var ajax_table = $("#crudTable").DataTable();
// if there are no navbars, return
if (!filtersNavbar.length) {
return;
}
// replace the datatables ajax url with new_url and reload it
ajax_table.ajax.url(new_url).load();
// run the init function for each filter
filtersNavbar.forEach(function(navbar) {
let filters = navbar.querySelectorAll('li[filter-init-function]');
// clear all filters
$(".navbar-filters li[filter-name]").trigger('filter:clear');
if(filters.length === 0) {
return;
}
// remove filters from URL
crud.updateUrl(new_url);
});
document.addEventListener('backpack:filter:changed', function(event) {
// check if any of the filters are active
let anyActiveFilters = false;
filters.forEach(function(filter) {
if (filter.classList.contains('active')) {
anyActiveFilters = true;
}
});
if(anyActiveFilters === true) {
navbar.querySelector('.remove_filters_button').classList.remove('invisible');
}else{
navbar.querySelector('.remove_filters_button').classList.add('invisible');
}
});
filters.forEach(function(filter) {
let initFunction = filter.getAttribute('filter-init-function');
if (window[initFunction]) {
window[initFunction](filter, navbar);
}
});
if(filtersNavbar.length === 0) {
return;
}
// hide the Remove filters button when no filter is active
$(".navbar-filters li[filter-name]").on('filter:clear', function() {
var anyActiveFilters = false;
$(".navbar-filters li[filter-name]").each(function () {
if ($(this).hasClass('active')) {
anyActiveFilters = true;
// console.log('ACTIVE FILTER');
let removeFiltersButton = navbar.querySelector('.remove_filters_button');
if (removeFiltersButton) {
removeFiltersButton.addEventListener('click', function(e) {
e.preventDefault();
document.dispatchEvent(new Event('backpack:filters:cleared', {
detail: {
navbar: navbar,
filters: filters,
}
}));
filters.forEach(function(filter) {
filter.dispatchEvent(new CustomEvent('backpack:filter:clear', {
detail: {
clearAllFilters: true,
}
}));
});
});
}
});
if (anyActiveFilters == false) {
$('#remove_filters_button').addClass('invisible');
}
filters.forEach(function(filter) {
filter.addEventListener('backpack:filter:clear', function() {
let anyActiveFilters = false;
filters.forEach(function (filterInstance) {
if (filterInstance.classList.contains('active')) {
anyActiveFilters = true;
}
});
if (anyActiveFilters === false) {
removeFiltersButton?.classList.add('invisible');
}
});
});
});
});
});
</script>
@endpush
@endpush

0 comments on commit 2c9bea6

Please sign in to comment.