digitalmarketplace-frontend
Version:
Digital Marketplace Frontend contains assets and components used in Digital Marketplace projects
290 lines (286 loc) • 13.6 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 OptionTreeSummary {
$totalSelected;
$totalSelectedText;
itemNameSingular;
itemNamePlural;
constructor($summary, itemNameSingular, itemNamePlural) {
this.$totalSelected = $summary.find('.dm-option-tree__summary-total-selected');
this.$totalSelectedText = $summary.find('.dm-option-tree__summary-total-selected-text');
this.itemNameSingular = itemNameSingular;
this.itemNamePlural = itemNamePlural;
}
updateSelectedCount = (numberSelected) => {
this.$totalSelected.text(numberSelected);
if (numberSelected === 1) {
this.$totalSelectedText.text(`${this.itemNameSingular} selected`);
}
else {
this.$totalSelectedText.text(`${this.itemNamePlural} selected`);
}
};
}
class OptionTreeSection {
optionTree;
$section;
tier;
optionTreeSections;
$checkbox;
checkboxes;
isParent;
constructor(optionTree, $section, tier) {
this.optionTree = optionTree;
this.$section = $section;
this.tier = tier;
this.optionTreeSections = this.$section.find('> .dm-option-tree-details__text > .dm-option-tree-details').get().map((element) => new OptionTreeSection(this.optionTree, $(element), tier + 1));
this.checkboxes = this.$section.find('> .dm-option-tree-details__text > .govuk-form-group > .govuk-checkboxes > .govuk-checkboxes__item > .govuk-checkboxes__input').get().map((element) => $(element));
this.isParent = this.optionTreeSections.length > 0;
const checkbox = this.$section.find('> .dm-option-tree-details__summary-checkbox > .dm-option-tree-details__summary-text > .dm-option-tree-details__summary-text-label > .govuk-checkboxes__item > .govuk-checkboxes__input').get(0);
if (checkbox) {
this.$checkbox = $(checkbox);
}
}
init = () => {
this.openSectionIfSelected();
if (this.isParent) {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.init());
}
else {
this.checkboxes.forEach(($checkbox) => {
$checkbox.on('click', () => {
this.optionTree.updateTotals($checkbox.val(), $checkbox.is(':checked'));
});
});
}
if (this.$checkbox) {
// Stop checkbox/label clicks toggling the parent <details> (which collapses
// the section); expand/collapse stays on the chevron.
this.$checkbox.closest('.govuk-checkboxes__item').on('click', (event) => {
event.stopPropagation();
});
this.$checkbox.on('click', (event) => {
const $checkbox = $(event.target);
if (this.optionTree.independentSelection) {
// Selecting a section is a selection of the section itself: it neither
// selects nor expands its children. Any click clears descendant
// selections, so checking makes the section the sole selection in its
// subtree and unchecking clears the subtree entirely.
const isChecked = $checkbox.is(':checked');
if (this.isParent) {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.toggleAllCheckboxes(false));
}
else {
this.checkboxes.forEach(($leafCheckbox) => $leafCheckbox.prop('checked', false));
}
$checkbox.prop('checked', isChecked);
this.optionTree.updateAllParts();
return;
}
if ($checkbox.is(':checked')) {
this.openSections();
}
if (this.isParent) {
this.optionTree.updateAllTotals(this.optionTreeSections, $checkbox.is(':checked'));
}
else {
this.optionTree.updateAllTotals([this], $checkbox.is(':checked'));
}
});
}
};
selectedItems = () => {
return this.checkboxes.filter(($checkbox) => $checkbox.is(':checked'));
};
openSectionIfSelected = () => {
// In independent selection mode a section selected by itself stays collapsed;
// only open it when the selection sits somewhere in its subtree.
const shouldOpen = this.optionTree.independentSelection
? this.hasSelectedDescendants()
: this.countSelected() > 0;
if (shouldOpen && this.$section.attr('open') !== 'open') {
this.$section.attr('open', 'open');
}
};
hasSelectedDescendants = () => {
if (this.isParent) {
return this.optionTreeSections.some((optionTreeSection) => optionTreeSection.$checkbox?.is(':checked') ||
optionTreeSection.hasSelectedDescendants());
}
return this.selectedItems().length > 0;
};
openSections = () => {
if (this.$section.attr('open') !== 'open') {
this.$section.attr('open', 'open');
}
if (this.isParent) {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.openSections());
}
};
countSelected = () => {
// A section's own checkbox is a selection in its own right in independent
// selection mode.
let total = this.optionTree.independentSelection && this.$checkbox?.is(':checked') ? 1 : 0;
if (this.isParent) {
total += this.optionTreeSections.reduce((sectionTotal, optionTreeSection) => sectionTotal + optionTreeSection.countSelected(), 0);
}
else {
total += this.selectedItems().length;
}
return total;
};
isAllSelected = () => {
if (this.isParent) {
return this.optionTreeSections.every((optionTreeSection) => optionTreeSection.isAllSelected());
}
else {
return this.countSelected() == this.checkboxes.length;
}
};
selectedItemValues = () => {
const values = this.isParent
? this.optionTreeSections.map((optionTreeSection) => optionTreeSection.selectedItemValues()).flat()
: this.selectedItems().map(($checkbox) => $checkbox.val());
if (this.optionTree.independentSelection && this.$checkbox?.is(':checked')) {
values.push(this.$checkbox.val());
}
return values;
};
updateSectionLocks = (fromTier) => {
if (this.countSelected()) {
if (this.tier === fromTier) {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.toggleSectionLock(false));
}
else {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.updateSectionLocks(fromTier));
}
}
else {
this.toggleSectionLock(true);
}
};
updateSectionCheckbox = () => {
if (this.optionTree.independentSelection) {
// Derive state bottom-up so child section checkboxes are final before the
// parent reads them. A section is a selection in its own right: a tick
// only ever means the user selected the section itself, and any selection
// beneath it clears that tick. Descendant selections show no derived
// state on the parent at all.
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.updateSectionCheckbox());
if (this.$checkbox && this.hasSelectedDescendants()) {
this.$checkbox.prop('checked', false);
}
return;
}
this.$checkbox?.prop('checked', this.isAllSelected());
if (this.isParent) {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.updateSectionCheckbox());
}
};
toggleSectionLock = (isLocked) => {
if (this.isParent) {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.toggleSectionLock(isLocked));
}
else {
this.checkboxes.forEach((checkbox) => checkbox.prop('disabled', isLocked));
}
if (isLocked) {
this.$section.attr('disabled', 'disabled');
this.$section.attr('tabIndex', -1);
this.$section.removeAttr('open');
this.$checkbox?.prop('disabled', true);
this.$checkbox?.prop('checked', false);
}
else {
this.$section.removeAttr('disabled');
this.$section.removeAttr('tabIndex');
this.$checkbox?.prop('disabled', false);
}
};
toggleCheckbox = (value, isChecked) => {
if (this.isParent) {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.toggleCheckbox(value, isChecked));
}
else {
this.checkboxes.forEach(($checkbox) => {
if ($checkbox.val() === value) {
$checkbox.prop('checked', isChecked);
}
});
}
};
toggleAllCheckboxes = (isChecked) => {
if (this.$checkbox) {
this.$checkbox.prop('checked', isChecked);
}
if (this.isParent) {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.toggleAllCheckboxes(isChecked));
}
else {
this.checkboxes.forEach(($checkbox) => {
$checkbox.prop('checked', isChecked);
});
}
};
}
class OptionTree {
static moduleName = 'dm-option-tree';
$optionTree;
optionTreeSummary;
optionTreeSections;
tierLimit;
independentSelection;
currentCount = 0;
constructor($optionTree) {
this.$optionTree = $optionTree;
this.optionTreeSummary = new OptionTreeSummary(this.$optionTree.find('.dm-option-tree__summary'), this.$optionTree.data('itemNameSingular'), this.$optionTree.data('itemNamePlural'));
this.tierLimit = this.$optionTree.data('tierRestriction') || 0;
this.independentSelection = this.$optionTree.data('independentSelection') === true;
this.optionTreeSections = this.$optionTree.find('.dm-option-tree__options > .dm-option-tree-details').get().map((element) => new OptionTreeSection(this, $(element), 1));
}
init = () => {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.init());
this.updateAllParts();
};
countSelected = () => {
return new Set(this.optionTreeSections.map((optionTreeSection) => optionTreeSection.selectedItemValues()).flat()).size;
};
updateOptionTreeSummaryCount = () => {
this.currentCount = this.countSelected();
this.optionTreeSummary.updateSelectedCount(this.currentCount);
};
updateSectionLocks = () => {
if (!this.tierLimit)
return;
this.optionTreeSections.forEach((optionTreeSection) => {
if (this.currentCount === 0) {
optionTreeSection.toggleSectionLock(false);
}
else {
optionTreeSection.updateSectionLocks(this.tierLimit);
}
});
};
updateSectionsCheckboxes = () => {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.updateSectionCheckbox());
};
updateAllParts = () => {
this.updateSectionsCheckboxes();
this.updateOptionTreeSummaryCount();
this.updateSectionLocks();
};
updateTotals = (value, isChecked) => {
this.optionTreeSections.forEach((optionTreeSection) => optionTreeSection.toggleCheckbox(value, isChecked));
this.updateAllParts();
};
updateAllTotals = (optionTreeSections, isChecked) => {
optionTreeSections.forEach((optionTreeSection) => optionTreeSection.toggleAllCheckboxes(isChecked));
this.updateAllParts();
};
}
exports.OptionTree = OptionTree;
}));
//# sourceMappingURL=option-tree.bundle.js.map