UNPKG

digitalmarketplace-frontend

Version:

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

290 lines (286 loc) 13.6 kB
(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