digitalmarketplace-frontend
Version:
Digital Marketplace Frontend contains assets and components used in Digital Marketplace projects
177 lines (173 loc) • 8.31 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.DigitalMarketplaceFrontend = {}));
})(this, (function (exports) { 'use strict';
class OptionSelect {
static moduleName = 'dm-option-select';
$optionSelect;
$options;
$optionsContainer;
$optionList;
$allCheckboxes;
checkedCheckboxes;
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);
}
}
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;
let checkedString = '';
if (count > 0) {
checkedString = count + ' selected';
}
return checkedString;
}
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');
}
isCheckboxVisible($checkbox) {
const initialOptionContainerHeight = this.$optionsContainer.get(0)?.clientHeight;
const optionListOffsetTop = this.$optionList.get(0).getBoundingClientRect().top + document.body.scrollTop;
const distanceFromTopOfContainer = ($checkbox.get(0).getBoundingClientRect().top + document.body.scrollTop) - optionListOffsetTop;
return distanceFromTopOfContainer < initialOptionContainerHeight;
}
getVisibleCheckboxes() {
const visibleCheckboxes = this.$options.get().filter((checkbox) => this.isCheckboxVisible($(checkbox)));
// add an extra checkbox, if the label of the first is too long it collapses onto itself
if (visibleCheckboxes.length < this.$options.length) {
visibleCheckboxes.push(this.$options.get(visibleCheckboxes.length));
}
return visibleCheckboxes;
}
setupHeight() {
const optionsContainer = this.$optionsContainer.get(0);
const optionList = this.$optionList.get(0);
let initialOptionContainerHeight = optionsContainer.clientHeight;
let height = optionList.offsetHeight;
// check whether this is hidden by progressive disclosure,
// because height calculations won't work
if (optionsContainer.offsetParent === null) {
initialOptionContainerHeight = 200;
height = 200;
}
// Resize if the list is only slightly bigger than its container
if (height < initialOptionContainerHeight + 50) {
this.setContainerHeight(height + 1);
return;
}
// Resize to cut last item cleanly in half
const visibleCheckboxes = this.getVisibleCheckboxes();
const lastVisibleCheckbox = visibleCheckboxes[visibleCheckboxes.length - 1];
const position = (lastVisibleCheckbox?.parentNode).offsetTop; // parent element is relative
this.setContainerHeight(position + (parseFloat(window.getComputedStyle(lastVisibleCheckbox, null).height.replace('px', '')) / 1.5));
}
}
exports.OptionSelect = OptionSelect;
}));
//# sourceMappingURL=option-select.bundle.js.map