Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Agnostic" filters - decouple filters from datatables #5714

Merged
merged 6 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERY good catch! Do we need to do this now, or can we do it later, as a non-breaking change? Cause it's a pretty big thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do it later as non-breaking

// 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/[email protected]/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