Skip to content

Commit

Permalink
Merge pull request #1389 from maykinmedia/feature/2687-selected-multi…
Browse files Browse the repository at this point in the history
…select-dropdown

✨ [2687] Display selected statuses for multiselect filter
  • Loading branch information
alextreme authored Sep 17, 2024
2 parents f06df1b + bfdc0ef commit 72a58d5
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 122 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% load i18n form_tags %}

{# Wrapper for multiple filters #}
<div class="filter-bar">
<div class="filter-bar" id="filterBar">
<form class="form" method="{{ method }}"
{% if no_action %}action=""{% else %}action="{% firstof form_action request.path %}"{% endif %}
{% if id %}id="{{ id }}"{% endif %}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% load i18n l10n form_tags icon_tags %}
{% load i18n l10n form_tags icon_tags button_tags %}

<div class="filter-bar__multiselect-listbox multiselect-listbox">
<button id="selectButton" type="button" class="button button__select" aria-haspopup="listbox" aria-expanded="false">
<div class="filter-bar__multiselect-listbox multiselect-listbox" id="selectDropdownWrapper">
<button id="selectButton" type="button" class="button button__select" aria-haspopup="listbox" aria-expanded="false" aria-live="polite">
{% trans 'Status' %}:
{% icon icon="expand_more" icon_position="after" icon_outlined=True %}
</button>
Expand All @@ -11,16 +11,19 @@
<div class="checkbox" role="option">
<input type="checkbox" name="status" value="{{ status }}" id="id_status_{{ forloop.counter }}" class="checkbox__input">
<label class="checkbox__label" for="id_status_{{ forloop.counter }}">
{{ status }} <span class="frequencyCounter">({{ frequency }})</span>
<span class="ellipsis">{{ status }} </span><span class="frequency-counter">({{ frequency }})</span>
</label>
</div>
{% endfor %}
</div>
{# Submit button appears on select #}
<div class="form__actions form__actions--fullwidth" id="filterFormActions">
<button class="button button--primary hide" type="submit" value="" title="{% trans 'Toon resultaten' %}" aria-label="{% trans 'Toon resultaten' %}" id="filterCases">
{% trans 'Toon' %}<span class="filter-bar__frequency-sum" id="frequencySum">0</span>{% trans 'resultaten' %}
<button class="button button--primary hide" type="submit" title="{% trans 'Toon resultaten' %}" aria-label="{% trans 'Toon resultaten' %}" id="filterCases">
{% trans 'Toon' %}<span class="filter-bar__frequency-sum" id="frequencySum">0</span><span id="resultText">{% trans 'resultaten' %}</span>
</button>
</div>
</div>
<div class="form__actions form__actions--fullwidth form__actions--reset">
{% button bordered=False text=_("Wis alle filters") id="resetFilters" type="button" transparent=True primary=True %}
</div>
</div>
323 changes: 218 additions & 105 deletions src/open_inwoner/js/components/FilterBar/multiselect_listbox_checkbox.js
Original file line number Diff line number Diff line change
@@ -1,135 +1,248 @@
/**
* When HTMX replaces parts of the DOM with new content, the JavaScript attached to the old DOM does not apply to the new content unless we explicitly reapply it.
* To fix this, listen for htmx:afterSwap events that are triggered after new content is swapped into the DOM.
*/

// Initialize select behavior
function initSelectBehavior() {
const selectButton = document.getElementById('selectButton')
const listboxDropdown = document.getElementById('listboxDropdown')
let currentIndex = -1 // Arrow key navigation

if (selectButton) {
selectButton.addEventListener('click', () => {
const isExpanded = selectButton.getAttribute('aria-expanded') === 'true'
listboxDropdown.classList.toggle('show')
selectButton.setAttribute('aria-expanded', isExpanded ? 'false' : 'true')
})
document.addEventListener('DOMContentLoaded', function () {
initFilterBar() // Initialize everything on page load
})

selectButton.addEventListener('keydown', (e) => {
const items = listboxDropdown.querySelectorAll('label')
if (e.key === 'ArrowDown') {
e.preventDefault()
currentIndex = (currentIndex + 1) % items.length
items[currentIndex].focus()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
currentIndex = (currentIndex - 1 + items.length) % items.length
items[currentIndex].focus()
} else if (e.key === 'Escape') {
listboxDropdown.classList.remove('show')
selectButton.setAttribute('aria-expanded', 'false')
selectButton.focus()
}
})
}
function initFilterBar() {
const filterBar = document.getElementById('filterBar')

if (listboxDropdown) {
listboxDropdown.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault()
currentIndex = (currentIndex + 1) % listboxDropdown.children.length
listboxDropdown.children[currentIndex].focus()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
currentIndex =
(currentIndex - 1 + listboxDropdown.children.length) %
listboxDropdown.children.length
listboxDropdown.children[currentIndex].focus()
} else if (e.key === 'Escape') {
listboxDropdown.classList.remove('show')
selectButton.setAttribute('aria-expanded', 'false')
selectButton.focus()
if (filterBar) {
const initCheckboxStateFromURL = function () {
const urlParams = new URLSearchParams(window.location.search)
const checkboxes = document.querySelectorAll(
'.filter-bar .checkbox__input'
)

checkboxes.forEach((checkbox) => {
const value = checkbox.value
checkbox.checked = urlParams.getAll('status').includes(value)
})

calculateAndDisplayCheckedSum() // Update button and sum
}

const calculateAndDisplayCheckedSum = function () {
const checkboxes = document.querySelectorAll(
'.filter-bar .checkbox__input'
)
let sum = 0
let selectedFilters = []

checkboxes.forEach((checkbox) => {
if (checkbox.checked) {
const label = checkbox.nextElementSibling
selectedFilters.push(label.textContent.trim())
const frequencyCounter = label.querySelector('.frequency-counter')
const match = frequencyCounter.textContent.match(/\d+/)
const value = match ? parseInt(match[0]) : 0

if (!isNaN(value)) {
sum += value
}
}
})

const selectButton = document.getElementById('selectButton')
selectButton.innerHTML = '' // Clear the button content before appending new elements

let expandIcon = document.createElement('span')
expandIcon.classList.add('material-icons')
expandIcon.setAttribute('aria-hidden', 'true')
expandIcon.textContent = 'expand_more'

let closeIcon = document.createElement('span')
closeIcon.classList.add('material-icons', 'close-icon')
closeIcon.setAttribute('aria-hidden', 'true')
closeIcon.textContent = 'close'

// Add text and icons based on selected filters
if (selectedFilters.length === 0) {
selectButton.textContent = 'Status '
selectButton.appendChild(expandIcon)
selectButton.classList.remove('active')
} else if (selectedFilters.length === 1) {
const ellipsisSpan = document.createElement('span')
ellipsisSpan.classList.add('ellipsis')
ellipsisSpan.textContent = selectedFilters[0]
selectButton.appendChild(ellipsisSpan)
selectButton.appendChild(closeIcon)
selectButton.classList.add('active')
} else {
selectButton.textContent = 'Status '
const activeFilterSpan = document.createElement('span')
activeFilterSpan.classList.add('active-filters')
activeFilterSpan.textContent = `${selectedFilters.length} actieve filters`
selectButton.appendChild(activeFilterSpan)
selectButton.appendChild(closeIcon)
selectButton.classList.add('active')
}
})
}

calculateAndDisplayCheckedSum()
}
closeIcon.addEventListener('click', function (event) {
event.stopPropagation()
checkboxes.forEach((checkbox) => {
checkbox.checked = false
})
calculateAndDisplayCheckedSum() // Recalculate and update the button and sum
})

// Display sum of the frequency counters for checked checkboxes
function calculateAndDisplayCheckedSum() {
const checkboxes = document.querySelectorAll('.filter-bar .checkbox__input')
let sum = 0
selectButton.setAttribute('aria-live', 'polite')

checkboxes.forEach((checkbox) => {
if (checkbox.checked) {
const label = checkbox.nextElementSibling
const frequencyCounter = label.querySelector('.frequencyCounter')
const match = frequencyCounter.textContent.match(/\d+/)
const value = match ? parseInt(match[0]) : 0
const frequencySumElement = document.getElementById('frequencySum')
const resultTextElement = document.getElementById('resultText')

if (!isNaN(value)) {
sum += value
if (frequencySumElement) {
frequencySumElement.textContent = sum
}

if (resultTextElement) {
resultTextElement.textContent = sum === 1 ? 'resultaat' : 'resultaten'
}

const filterCasesButton = document.getElementById('filterCases')
const filterFormActions = document.getElementById('filterFormActions')
const resetFilters = document.getElementById('resetFilters')

if (filterCasesButton && filterFormActions) {
if (sum > 0) {
filterCasesButton.classList.remove('hide')
filterFormActions.classList.remove('hide')
resetFilters.classList.remove('hide')
} else {
filterCasesButton.classList.add('hide')
filterFormActions.classList.add('hide')
resetFilters.classList.add('hide')
}
}
}
})

const frequencySumElement = document.getElementById('frequencySum')
const filterCasesButton = document.getElementById('filterCases')
const filterFormActions = document.getElementById('filterFormActions')
const initSelectBehavior = function () {
const selectButton = document.getElementById('selectButton')
const listboxDropdown = document.getElementById('listboxDropdown')
const selectDropdownWrapper = document.getElementById(
'selectDropdownWrapper'
)
let currentIndex = -1

if (selectButton) {
selectButton.addEventListener('click', () => {
const isExpanded =
selectButton.getAttribute('aria-expanded') === 'true'
listboxDropdown.classList.toggle('show')
selectButton.setAttribute(
'aria-expanded',
isExpanded ? 'false' : 'true'
)
})

selectButton.addEventListener('keydown', (e) => {
const items = listboxDropdown.querySelectorAll(
'.filter-bar .checkbox__label'
)
if (e.key === 'ArrowDown') {
e.preventDefault()
currentIndex = (currentIndex + 1) % items.length
items[currentIndex].focus()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
currentIndex = (currentIndex - 1 + items.length) % items.length
items[currentIndex].focus()
} else if (e.key === 'Escape') {
listboxDropdown.classList.remove('show')
selectButton.setAttribute('aria-expanded', 'false')
selectButton.focus()
}
})
}

// Display the sum
if (frequencySumElement) {
frequencySumElement.textContent = sum
}
if (listboxDropdown) {
listboxDropdown.addEventListener('keydown', (e) => {
const items = listboxDropdown.querySelectorAll(
'.filter-bar .checkbox__label'
)
if (e.key === 'ArrowDown') {
e.preventDefault()
currentIndex = (currentIndex + 1) % items.length
items[currentIndex].focus()
} else if (e.key === 'ArrowUp') {
e.preventDefault()
currentIndex = (currentIndex - 1 + items.length) % items.length
items[currentIndex].focus()
} else if (e.key === 'Escape') {
listboxDropdown.classList.remove('show')
selectButton.setAttribute('aria-expanded', 'false')
selectButton.focus()
}
})
}

// Close dropdown when clicking outside the selectButton and listboxDropdown
document.addEventListener('click', function (e) {
if (!selectDropdownWrapper.contains(e.target)) {
listboxDropdown.classList.remove('show')
selectButton.setAttribute('aria-expanded', 'false')
}
})

// Ensure the filterCasesButton and filterFormActions exist before modifying them
if (filterCasesButton && filterFormActions) {
if (sum > 0) {
filterCasesButton.classList.remove('hide')
filterFormActions.classList.remove('hide')
} else {
filterCasesButton.classList.add('hide')
filterFormActions.classList.add('hide')
initCheckboxStateFromURL()
}
}
}

// Listen for checkbox change events to update the sum
document.addEventListener('change', function (e) {
if (e.target && e.target.classList.contains('checkbox__input')) {
calculateAndDisplayCheckedSum() // Update sum on toggle
}
})
document.addEventListener('change', function (e) {
if (e.target && e.target.classList.contains('checkbox__input')) {
if (e.target.closest('.filter-bar')) {
calculateAndDisplayCheckedSum()
}
}
})

// Listen for the htmx:afterSwap event to initialize the select behavior and recalculate sum
document.body.addEventListener('htmx:afterSwap', function () {
const listboxDropdown = document.getElementById('listboxDropdown')
const dataMultiSelectLabel = document.querySelectorAll('.checkbox__label')
const resetFilters = document.getElementById('resetFilters')
if (resetFilters) {
resetFilters.addEventListener('click', function (e) {
e.preventDefault()
const checkboxes = document.querySelectorAll(
'.filter-bar .checkbox__input'
)
checkboxes.forEach((checkbox) => {
checkbox.checked = false
})

calculateAndDisplayCheckedSum()

const filterBarForm = document.querySelector('#filterBar .form')
if (filterBarForm) {
filterBarForm.submit()
}
})
}

if (listboxDropdown && dataMultiSelectLabel) {
initSelectBehavior()
}

calculateAndDisplayCheckedSum()
})
document.body.addEventListener('htmx:afterSwap', function () {
initFilterBar()
calculateAndDisplayCheckedSum() // Make sure sum is updated after swap
})

// Run on page load to initialize the component
document.addEventListener('DOMContentLoaded', function () {
initSelectBehavior()
})
document.addEventListener('DOMContentLoaded', function () {
initSelectBehavior()
})
}
}

// Scroll to the top of the DOM
function scrollToTopOfWindow() {
setTimeout(function () {
window.scrollTo({ top: 0, behavior: 'smooth' })
}, 200) // Allow more time for HTMX to load
}, 10)
}

// Listen for clicks on any pagination to scroll to the top of the DOM window when HTMX refresh is triggered
document.body.addEventListener('htmx:afterSwap', function () {
setTimeout(function () {
initFilterBar() // Reinitialize filter bar after swap
}, 50)
})

document.addEventListener('click', function (e) {
if (e.target && e.target.classList.contains('pagination__link')) {
scrollToTopOfWindow()
setTimeout(function () {
initFilterBar() // Reinitialize filter bar after swap
}, 20)
}
})
Loading

0 comments on commit 72a58d5

Please sign in to comment.