digitalmarketplace-frontend
Version:
Digital Marketplace Frontend contains assets and components used in Digital Marketplace projects
219 lines (217 loc) • 8.87 kB
JavaScript
class OptionSelect {
static moduleName = 'dm-option-select';
$optionSelect;
$options;
$optionsContainer;
$optionList;
$allCheckboxes;
checkedCheckboxes;
filterOptions;
constructor($module) {
this.$optionSelect = $module;
this.$options = this.$optionSelect.find('input[type="checkbox"]');
this.$optionsContainer = this.$optionSelect.find('.js-options-container');
this.$optionList = this.$optionsContainer.find('.js-auto-height-inner');
this.$allCheckboxes = this.$optionsContainer.find('.govuk-checkboxes__item');
this.checkedCheckboxes = [];
}
init() {
// Attach listener to update checked count
this.$optionSelect.on('change', (event) => {
const changedEl = event.target;
if (changedEl.tagName === 'INPUT' && changedEl.getAttribute('type') === 'checkbox') {
this.updateCheckedCount();
}
});
// Replace div.container-head with a button
this.replaceHeadingSpanWithButton();
// Add js-collapsible class to parent for CSS
this.$optionSelect.addClass('js-collapsible');
// Add open/close listeners
this.$optionSelect.find('.js-container-button').on('click', (event) => {
this.toggleOptionSelect(event);
});
if (this.$optionSelect.data().closedOnLoad) {
this.close();
}
else {
this.setupHeight();
}
const checkedString = this.checkedString();
if (checkedString) {
this.attachCheckedCounter(checkedString);
}
const $filter = this.$optionSelect.find('.js-dm-filter-options');
if ($filter.length) {
this.filterOptions = new FilterOptions($filter, this);
this.filterOptions.init();
}
}
getAllCheckedCheckboxes() {
this.checkedCheckboxes = [];
this.$allCheckboxes.each((_index, checkbox) => {
if ($(checkbox).find('input[type=checkbox]').is(':checked')) {
this.checkedCheckboxes.push(checkbox);
}
});
}
replaceHeadingSpanWithButton() {
/* Replace the span within the heading with a button element. This is based on feedback from Léonie Watson.
* The button has all of the accessibility hooks that are used by screen readers and etc.
* We do this in the JavaScript because if the JavaScript is not active then the button shouldn't
* be there as there is no JS to handle the click event.
*/
const $containerHead = this.$optionSelect.find('.js-container-button');
const jsContainerHeadHTML = $containerHead.html();
// Create button and replace the preexisting html with the button.
const $button = document.createElement('button');
$button.classList.add('js-container-button', 'dm-option-select__title', 'dm-option-select__button');
// Add type button to override default type submit when this component is used within a form
$button.setAttribute('type', 'button');
$button.setAttribute('aria-expanded', 'true');
$button.setAttribute('id', $containerHead.attr('id'));
$button.setAttribute('aria-controls', this.$optionsContainer.attr('id'));
$button.innerHTML = jsContainerHeadHTML;
$containerHead.after($button.outerHTML);
$containerHead.remove();
}
attachCheckedCounter(checkedString) {
this.$optionSelect.find('.js-container-button').after(`<div class="dm-option-select__selected-counter js-selected-counter">${checkedString}</div>`);
}
updateCheckedCount() {
const checkedString = this.checkedString();
const $checkedStringElement = this.$optionSelect.find('.js-selected-counter');
if (checkedString) {
if ($checkedStringElement.length) {
$checkedStringElement.text(checkedString);
}
else {
this.attachCheckedCounter(checkedString);
}
}
else {
$checkedStringElement.remove();
}
}
checkedString() {
this.getAllCheckedCheckboxes();
const count = this.checkedCheckboxes.length;
return count > 0 ? `${count} selected` : '';
}
toggleOptionSelect(event) {
if (this.isClosed()) {
this.open();
}
else {
this.close();
}
event.preventDefault();
}
open() {
if (this.isClosed()) {
this.$optionSelect.find('.js-container-button').attr('aria-expanded', 'true');
this.$optionSelect.removeClass('js-closed');
this.$optionSelect.addClass('js-opened');
if (!this.$optionsContainer.css('height')) {
this.setupHeight();
}
}
}
close() {
this.$optionSelect.removeClass('js-opened');
this.$optionSelect.addClass('js-closed');
this.$optionSelect.find('.js-container-button').attr('aria-expanded', 'false');
}
isClosed() {
return this.$optionSelect.hasClass('js-closed');
}
setContainerHeight(height) {
this.$optionsContainer.css('height', height + 'px');
}
getMaxContainerHeight() {
const maxHeight = parseFloat(window.getComputedStyle(this.$optionsContainer.get(0)).maxHeight);
return Number.isNaN(maxHeight) ? 200 : maxHeight;
}
isCheckboxVisible($checkbox, containerHeight) {
if (!$checkbox.is(':visible')) {
return false;
}
const checkboxTop = $checkbox.get(0).offsetTop;
return checkboxTop < containerHeight;
}
getVisibleCheckboxes(containerHeight) {
const allVisibleCheckboxes = this.$allCheckboxes.filter(':visible').get();
const visibleCheckboxes = allVisibleCheckboxes.filter((checkbox) => this.isCheckboxVisible($(checkbox), containerHeight));
// Add one extra visible row so the collapsed list hints there are more options.
if (visibleCheckboxes.length < allVisibleCheckboxes.length) {
visibleCheckboxes.push(allVisibleCheckboxes[visibleCheckboxes.length]);
}
return visibleCheckboxes;
}
setupHeight() {
const optionsContainer = this.$optionsContainer.get(0);
const optionList = this.$optionList.get(0);
const maxContainerHeight = this.getMaxContainerHeight();
const height = optionList.scrollHeight;
// check whether this is hidden by progressive disclosure,
// because height calculations won't work
if (optionsContainer.offsetParent === null) {
this.setContainerHeight(maxContainerHeight);
return;
}
// Resize if the list is only slightly bigger than its container
if (height <= maxContainerHeight + 50) {
this.setContainerHeight(height + 1);
return;
}
// Resize to cut last item cleanly in half
const visibleCheckboxes = this.getVisibleCheckboxes(maxContainerHeight);
if (!visibleCheckboxes.length) {
this.setContainerHeight(height + 1);
return;
}
const lastVisibleCheckbox = visibleCheckboxes[visibleCheckboxes.length - 1];
const position = lastVisibleCheckbox?.offsetTop;
const checkboxHeight = lastVisibleCheckbox?.offsetHeight ?? 0;
// Never shrink below one full visible row.
const calculatedHeight = position + (checkboxHeight / 1.5);
this.setContainerHeight(Math.max(calculatedHeight, checkboxHeight + 1));
}
}
class FilterOptions {
static moduleName = 'dm-filter-options';
$filter;
$optionSelect;
$optionsContainer;
$input;
$checkboxes;
optionSelect;
constructor($module, optionSelect) {
this.$filter = $module;
this.$optionSelect = optionSelect.$optionSelect;
this.$optionsContainer = optionSelect.$optionsContainer;
this.$input = this.$filter.find('input');
this.$checkboxes = this.$optionSelect.find('.govuk-checkboxes__item');
this.optionSelect = optionSelect;
}
init() {
this.$input.on('keyup input search', () => this.filterCheckboxes());
}
filterCheckboxes() {
const query = this.$input.val().toLowerCase();
this.$checkboxes.each((_index, checkbox) => {
const $checkbox = $(checkbox);
const label = $checkbox.find('label').text().toLowerCase();
if (label.includes(query)) {
$checkbox.show();
}
else {
$checkbox.hide();
}
});
this.$optionsContainer.css('height', 'auto');
this.optionSelect.setupHeight();
}
}
export { OptionSelect };
//# sourceMappingURL=option-select.bundle.mjs.map