-
Notifications
You must be signed in to change notification settings - Fork 911
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
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
@@ -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(/&/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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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