UNPKG

digitalmarketplace-frontend

Version:

Digital Marketplace Frontend contains assets and components used in Digital Marketplace projects

219 lines (217 loc) 8.87 kB
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