Skip to content

Commit

Permalink
[#4081] Add eventListener to toggle the dropdown of the multi combobox
Browse files Browse the repository at this point in the history
Rename data-toggle to data-bs-toggle

Co-authored-by: Jane Sandberg <[email protected]>
  • Loading branch information
christinach and sandbergja committed Nov 13, 2024
1 parent c6b6f0a commit 468cd2b
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 115 deletions.
2 changes: 1 addition & 1 deletion app/components/multiselect_combobox_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="mb-3 advanced-search-facet row dropdown">
<label class="col-sm-4 control-label advanced-facet-label" for="<%= @dom_id %>"><%= @label %></label>
<input id="<%= @dom_id %>" data-toggle="dropdown" autocomplete="off"
<input id="<%= @dom_id %>" data-bs-toggle="dropdown" autocomplete="off"
class="col-sm-8 combobox-multiselect" role="combobox"
aria-expanded="false" aria-controls="<%= @listbox_id %>">
<span class="fa fa-caret-down" aria-hidden="true"></span>
Expand Down
233 changes: 123 additions & 110 deletions app/javascript/orangelight/multiselect_combobox.es6
Original file line number Diff line number Diff line change
@@ -1,133 +1,146 @@
import SelectedOptions from "./selected_options.es6"
import SelectedOptions from './selected_options.es6';

// This class is responsible for providing a multi-select combobox
// to the user and recording their selections in a hidden <select>
// element. Loosely based on the Multiselect with comma-separated
// values from this article: https://www.24a11y.com/2019/select-your-poison-part-2/
export default class MultiselectCombobox {
constructor(inputElement) {
this.selectedOptions = new SelectedOptions();
this.inputElement = inputElement;
this.hiddenSelect = inputElement.closest('.dropdown').querySelector('select')
this.listElement = inputElement.closest('.dropdown').querySelector('ul')
this.numberOfResultsElement = inputElement.closest('.dropdown').querySelector('.number-of-results')
this.#addEventListeners();
this.#applySelections();
}
constructor(inputElement) {
this.selectedOptions = new SelectedOptions();
this.inputElement = inputElement;
this.hiddenSelect = inputElement
.closest('.dropdown')
.querySelector('select');
this.listElement = inputElement.closest('.dropdown').querySelector('ul');
this.numberOfResultsElement = inputElement
.closest('.dropdown')
.querySelector('.number-of-results');
this.#addEventListeners();
this.#applySelections();
}

toggleItem(item) {
this.#toggleListItem(item)
this.selectedOptions.toggle(item.firstChild.nodeValue)
this.inputElement.value = this.selectedOptions.toString();
this.#updateHiddenSelect();
this.#orderList();
}
toggleItem(item) {
this.#toggleListItem(item);
this.selectedOptions.toggle(item.firstChild.nodeValue);
this.inputElement.value = this.selectedOptions.toString();
this.#updateHiddenSelect();
this.#orderList();
}

updateOptionVisibility() {
let numberOfResults = 0;
const queries = this.inputElement.value.split(';');
this.listElement.querySelectorAll('li').forEach(item => {
if(queries.some(query => {
const normalizedQuery = query.trim().toLowerCase();
return item.textContent.toLowerCase().includes(normalizedQuery);
})) {
item.classList.remove('d-none')
numberOfResults++;
} else {
item.classList.add('d-none')
}
updateOptionVisibility() {
let numberOfResults = 0;
const queries = this.inputElement.value.split(';');
this.listElement.querySelectorAll('li').forEach((item) => {
if (
queries.some((query) => {
const normalizedQuery = query.trim().toLowerCase();
return item.textContent.toLowerCase().includes(normalizedQuery);
})
this.numberOfResultsElement.textContent = (numberOfResults === 1) ? '1 option. Press down arrow for options.' : `${numberOfResults} options. Press down arrow for options.`
}
) {
item.classList.remove('d-none');
numberOfResults++;
} else {
item.classList.add('d-none');
}
});
this.numberOfResultsElement.textContent =
numberOfResults === 1
? '1 option. Press down arrow for options.'
: `${numberOfResults} options. Press down arrow for options.`;
}

#addEventListeners() {
this.listElement.querySelectorAll('li').forEach((item) => {
item.addEventListener('keyup', (event) => {
if (event.code == 'Enter') {
this.toggleItem(item)
} else {
// Send all other events to the input, so that
// anything the user types ends up there
this.inputElement.dispatchEvent(new KeyboardEvent('keyup', {key: event.key, code: event.code}))
}
})
item.addEventListener('click', (event) => {
this.toggleItem(item)

// Don't propagate the event to the bootstrap event
// listener. Otherwise, the dropdown closes every
// time the user clicks on an item
event.stopPropagation();
})
})
this.inputElement.addEventListener('input', (event) => {
this.updateOptionVisibility();
this.#openDropdownIfClosed();
})
}
#addEventListeners() {
this.listElement.querySelectorAll('li').forEach((item) => {
item.addEventListener('keyup', (event) => {
if (event.code == 'Enter') {
this.toggleItem(item);
} else {
// Send all other events to the input, so that
// anything the user types ends up there
this.inputElement.dispatchEvent(
new KeyboardEvent('keyup', { key: event.key, code: event.code })
);
}
});
item.addEventListener('click', (event) => {
this.toggleItem(item);
// Don't propagate the event to the bootstrap event
// listener. Otherwise, the dropdown closes every
// time the user clicks on an item
event.stopPropagation();
});
});
this.inputElement.addEventListener('input', (event) => {
this.updateOptionVisibility();
this.#openDropdownIfClosed();
});
}

#applySelections() {
this.hiddenSelect.querySelectorAll('option:checked').forEach(selectedOption => {
this.toggleItem(this.#getListItemByText(selectedOption.textContent))
})
}
#applySelections() {
this.hiddenSelect
.querySelectorAll('option:checked')
.forEach((selectedOption) => {
this.toggleItem(this.#getListItemByText(selectedOption.textContent));
});
}

#toggleListItem(item) {
const icon = `<span class="fa fa-check" aria-hidden="true"></span>`
#toggleListItem(item) {
const icon = `<span class="fa fa-check" aria-hidden="true"></span>`;

if(this.selectedOptions.contains(item.firstChild.nodeValue)) {
item.querySelectorAll('span').forEach(span => span.remove())
item.classList.remove('active')
item.setAttribute('aria-selected', 'false')
} else {
item.innerHTML += icon
item.classList.add('active')
item.setAttribute('aria-selected', 'true')
}
if (this.selectedOptions.contains(item.firstChild.nodeValue)) {
item.querySelectorAll('span').forEach((span) => span.remove());
item.classList.remove('active');
item.setAttribute('aria-selected', 'false');
} else {
item.innerHTML += icon;
item.classList.add('active');
item.setAttribute('aria-selected', 'true');
}
}

#updateHiddenSelect() {
this.hiddenSelect.querySelectorAll('option').forEach((option) => {
if (this.selectedOptions.contains(option.textContent.trim())) {
option.setAttribute('selected', 'selected')
} else {
option.removeAttribute('selected')
}
})
}
#updateHiddenSelect() {
this.hiddenSelect.querySelectorAll('option').forEach((option) => {
if (this.selectedOptions.contains(option.textContent.trim())) {
option.setAttribute('selected', 'selected');
} else {
option.removeAttribute('selected');
}
});
}

#getListItemByText(text) {
return Array.from(this.listElement.children).find((item) => {
return item.textContent.trim() === text.trim()
})
}
#getListItemByText(text) {
return Array.from(this.listElement.children).find((item) => {
return item.textContent.trim() === text.trim();
});
}

// Note: Bootstrap 4 requires jquery to open a dropdown.
// When we move to Bootstrap 5, we should use vanilla js
// here instead
#openDropdownIfClosed() {
if (!this.listElement.classList.contains('show')) {
$(`#${this.inputElement.id}`).dropdown('toggle')
}
#openDropdownIfClosed() {
if (!this.listElement.classList.contains('show')) {
$(`#${this.inputElement.id}`).dropdown('toggle');
}
}

#orderList() {
[].slice.call(this.listElement.children)
.sort(this.#compare)
.forEach(function(val, i) {
this.listElement.appendChild(val);
}, this);
}
#orderList() {
[].slice
.call(this.listElement.children)
.sort(this.#compare)
.forEach(function (val, i) {
this.listElement.appendChild(val);
}, this);
}

#compare(a, b) {
function toBoolean(value) {
return value === 'true' ? true : false;
}
#compare(a, b) {
function toBoolean(value) {
return value === 'true' ? true : false;
}

if (toBoolean(a.getAttribute('aria-selected')) !== toBoolean(b.getAttribute('aria-selected'))) {
return toBoolean(a.getAttribute('aria-selected')) ? -1 : 1;
} else {
return a.textContent.localeCompare(b.textContent);
}
if (
toBoolean(a.getAttribute('aria-selected')) !==
toBoolean(b.getAttribute('aria-selected'))
) {
return toBoolean(a.getAttribute('aria-selected')) ? -1 : 1;
} else {
return a.textContent.localeCompare(b.textContent);
}
}
}
2 changes: 1 addition & 1 deletion app/views/catalog/_per_page_widget.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<span class="visually-hidden"><%= t('blacklight.search.per_page.title') %></span>
<div id="per_page-dropdown" class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<button type="button" class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown">
<%= t(:'blacklight.search.per_page.button_label', :count => current_per_page).html_safe %> <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
Expand Down
2 changes: 1 addition & 1 deletion app/views/catalog/_show_tools.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</li>
<% end %>
<li class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle" data-toggle="dropdown"><span class="icon-share" aria-hidden="true"></span> Send <span class="d-none d-lg-inline">to <span class="caret"></span></span></button>
<button class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown"><span class="icon-share" aria-hidden="true"></span> Send <span class="d-none d-lg-inline">to <span class="caret"></span></span></button>
<ul class="dropdown-menu position-absolute">
<li class="sms">
<%= link_to "SMS", sms_solr_document_path(:id => @document), {:id => 'smsLink', :data => {:blacklight_modal => "trigger"}, class: "icon-mobile dropdown-item", rel: 'nofollow'} %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/catalog/_sort_widget.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<% if show_sort_and_per_page? and !active_sort_fields.blank? %>
<div id="sort-dropdown" class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<button type="button" class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown">
<%= t('blacklight.search.sort.label', :field =>current_sort_field.label).html_safe %> <span class="caret"></span>
</button>

Expand Down
2 changes: 1 addition & 1 deletion app/views/shared/_login.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<p class="or-divider">
or
</p>
<p><a role="button" data-toggle="collapse" href="#collapseAlma" aria-expanded="false" aria-controls="collapseAlma" class="btn btn-outline-secondary">
<p><a role="button" data-bs-toggle="collapse" href="#collapseAlma" aria-expanded="false" aria-controls="collapseAlma" class="btn btn-outline-secondary">
<%= t('blacklight.login.alma_login_msg') %>
</a></p>
</div>
Expand Down

0 comments on commit 468cd2b

Please sign in to comment.