Files
notifications-admin/app/assets/javascripts/collapsibleCheckboxes.js

243 lines
8.0 KiB
JavaScript
Raw Normal View History

Removed all govuk css (#2814) * Removed all govuk css * Updated reference files * Removing govuk js * Fixed casing for modules, removed unused page * Got more reference images * Updated template page * Removed govuk padding util * Updated hint to uswds hint * More govuk cleanup * Commiting backstopjs ref files * Fixed all unit tests that broke due to brittleness around govuk styling * Added new ref images * Final removal of govuk * Officially removed all govuk references * Updated reference file * Updated link to button * UI modernization * Cleanup * removed govuk escaping tests since they are no longer needed * Fix CodeQL security issue in escapeElementName function - Escape backslashes first before other special characters - Prevents potential double-escaping vulnerability - Addresses CodeQL alert about improper string escaping * Found more govuk removal. Fixed unit tests * Add missing pipeline check to pre-commit * updated test * Updated e2e test * More update to e2e test * Fixed another e2e test * Simple PR comments addressed * More updates * Updated backstop ref files * Refactored folder selection for non-admins * Updated redundant line * Updated tests to include correct mocks * Added more ref files * Addressing carlos comments * Addressing Carlo comments, cleanup of window initing * More cleanup and addressing carlo comments * Fixing a11 scan * Fixed a few issues with javascript * Fixed for pr * Fixing e2e tests * Tweaking e2e test * Added more ref files and cleaned up urls.js * Fixed bug with creating new template * Removed brittle test - addressed code ql comment * e2e race condition fix * More e2e test fixes * Updated e2e tests to not wait for text sent * Updated test to not wait for button click response * Made tear down more resilent if staging is down * reverted e2e test to what was working before main merge * Updated backstopRef images * Updated gulp to include job-polling differently
2025-10-06 09:38:54 -04:00
(function (window) {
"use strict";
function Summary (module) {
this.module = module;
2025-10-22 08:49:49 -04:00
this.el = module.formGroup.querySelector('.selection-summary');
this.fieldLabel = module.fieldLabel;
this.total = module.total;
this.addContent();
this.update(module.getSelection());
}
Summary.prototype.templates = {
all: (selection, total, field) => `All ${field}s`,
some: (selection, total, field) => `${selection} of ${total} ${field}s`,
none: (selection, total, field) => ({
"folder": "No folders (only templates outside a folder)",
"team member": "No team members (only you)"
}[field] || `No ${field}s`)
};
Summary.prototype.addContent = function() {
2025-10-22 08:49:49 -04:00
const hint = this.module.formGroup.querySelector('.usa-hint');
this.text = document.createElement('p');
this.text.className = 'selection-summary__text';
2025-10-22 08:49:49 -04:00
if (this.fieldLabel === 'folder') { this.text.classList.add('selection-summary__text--folders'); }
2025-10-28 17:42:25 -04:00
if (hint) {
this.el.setAttribute('id', hint.getAttribute('id'));
hint.remove();
}
2025-10-22 08:49:49 -04:00
this.el.appendChild(this.text);
};
Summary.prototype.update = function(selection) {
let template;
if (selection === this.total) {
template = 'all';
} else if (selection > 0) {
template = 'some';
} else {
template = 'none';
}
2025-10-22 08:49:49 -04:00
this.text.innerHTML = this.templates[template](selection, this.total, this.fieldLabel);
};
Summary.prototype.bindEvents = function () {
// take summary out of tab order when focus moves
2025-10-22 08:49:49 -04:00
this.el.addEventListener('blur', (e) => e.target.setAttribute('tabindex', '-1'));
};
function Footer (module) {
this.module = module;
this.fieldLabel = module.fieldLabel;
2025-10-22 08:49:49 -04:00
this.fieldsetId = module.fieldset.getAttribute('id');
this.el = this.getEl(this.module.expanded);
this.module.formGroup.appendChild(this.el);
}
Footer.prototype.buttonContent = {
change: (fieldLabel) => `Choose ${fieldLabel}s`,
2023-06-16 14:55:24 -04:00
done: (fieldLabel) => `Done<span class="usa-sr-only"> choosing ${fieldLabel}s</span>`
};
Footer.prototype.getEl = function (expanded) {
const buttonState = expanded ? 'done' : 'change';
const buttonContent = this.buttonContent[buttonState](this.fieldLabel);
const stickyClass = expanded ? ' js-stick-at-bottom-when-scrolling' : '';
2025-10-22 08:49:49 -04:00
const div = document.createElement('div');
div.className = `selection-footer${stickyClass} margin-top-2`;
div.innerHTML = `<button
2025-07-02 18:14:22 -04:00
class="usa-button usa-button--outline selection-footer__button"
aria-expanded="${expanded ? 'true' : 'false'}"
aria-controls="${this.fieldsetId}">
${buttonContent}
2025-10-22 08:49:49 -04:00
</button>`;
return div;
};
Footer.prototype.update = function (expanded) {
2025-10-22 08:49:49 -04:00
this.el.remove();
this.el = this.getEl(expanded);
2025-10-22 08:49:49 -04:00
this.module.formGroup.appendChild(this.el);
};
function CollapsibleCheckboxes () {}
2025-10-22 08:49:49 -04:00
CollapsibleCheckboxes.prototype._focusTextElement = (el) => {
el.setAttribute('tabindex', '-1');
el.focus();
};
CollapsibleCheckboxes.prototype.start = function(component) {
2025-10-22 08:49:49 -04:00
this.component = component;
this.formGroup = component.querySelector('.usa-form-group');
this.fieldset = this.formGroup.querySelector('fieldset');
this.checkboxes = this.fieldset.querySelectorAll('input[type=checkbox]');
this.fieldLabel = component.dataset.fieldLabel;
this.total = this.checkboxes.length;
this.legendText = this.fieldset.querySelector('legend').textContent.trim();
this.expanded = false;
this.checkUncheckButtonsAdded = false;
this.addHeadingHideLegend();
// generate summary and footer
this.footer = new Footer(this);
this.summary = new Summary(this);
2025-10-22 08:49:49 -04:00
this.fieldset.insertAdjacentElement('beforebegin', this.summary.el);
// add custom classes
2025-10-22 08:49:49 -04:00
this.formGroup.classList.add('selection-wrapper');
this.fieldset.classList.add('selection-content');
// hide checkboxes
2025-10-22 08:49:49 -04:00
this.fieldset.style.display = 'none';
this.bindEvents();
};
2025-10-22 08:49:49 -04:00
CollapsibleCheckboxes.prototype.getSelection = function() {
return Array.from(this.checkboxes).filter(cb => cb.checked).length;
};
CollapsibleCheckboxes.prototype.addHeadingHideLegend = function() {
2025-10-22 08:49:49 -04:00
const headingLevel = this.component.dataset.headingLevel || '2';
2025-10-22 08:49:49 -04:00
this.heading = document.createElement(`h${headingLevel}`);
this.heading.className = 'heading-small';
this.heading.textContent = this.legendText;
this.fieldset.insertAdjacentElement('beforebegin', this.heading);
2025-10-22 08:49:49 -04:00
this.fieldset.querySelector('legend').classList.add('usa-sr-only');
};
CollapsibleCheckboxes.prototype.addCheckUncheckAllButtons = function() {
2025-10-22 08:49:49 -04:00
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'check-uncheck-all-buttons margin-bottom-2';
2025-10-22 08:49:49 -04:00
this.toggleAllButton = document.createElement('button');
this.toggleAllButton.type = 'button';
this.toggleAllButton.className = 'usa-button usa-button--outline usa-button--small';
this.toggleAllButton.textContent = 'Select all';
2025-10-22 08:49:49 -04:00
buttonsContainer.appendChild(this.toggleAllButton);
2025-10-22 08:49:49 -04:00
this.summary.el.insertAdjacentElement('afterend', buttonsContainer);
2025-10-22 08:49:49 -04:00
this.toggleAllButton.addEventListener('click', this.toggleAll.bind(this));
this.updateToggleButtonText();
};
CollapsibleCheckboxes.prototype.toggleAll = function(e) {
e.preventDefault();
2025-08-19 13:16:52 -07:00
e.stopPropagation();
2025-10-22 08:49:49 -04:00
const allChecked = Array.from(this.checkboxes).filter(cb => cb.checked).length === this.checkboxes.length;
if (allChecked) {
2025-10-22 08:49:49 -04:00
this.checkboxes.forEach(cb => cb.checked = false);
} else {
2025-10-22 08:49:49 -04:00
this.checkboxes.forEach(cb => cb.checked = true);
}
this.handleSelection();
this.updateToggleButtonText();
};
CollapsibleCheckboxes.prototype.updateToggleButtonText = function() {
2025-10-22 08:49:49 -04:00
if (!this.toggleAllButton) return;
2025-10-22 08:49:49 -04:00
const checkedCount = Array.from(this.checkboxes).filter(cb => cb.checked).length;
const allChecked = checkedCount === this.checkboxes.length;
if (allChecked) {
2025-10-22 08:49:49 -04:00
this.toggleAllButton.textContent = 'Deselect all';
} else {
2025-10-22 08:49:49 -04:00
this.toggleAllButton.textContent = 'Select all';
}
};
CollapsibleCheckboxes.prototype.expand = function(e) {
if (e !== undefined) { e.preventDefault(); }
if (!this.expanded) {
2025-10-22 08:49:49 -04:00
this.fieldset.style.display = '';
this.expanded = true;
this.summary.update(this.getSelection());
this.footer.update(this.expanded);
if (!this.checkUncheckButtonsAdded) {
this.addCheckUncheckAllButtons();
this.checkUncheckButtonsAdded = true;
} else {
2025-10-22 08:49:49 -04:00
if (this.toggleAllButton) {
this.toggleAllButton.parentElement.style.display = '';
}
}
}
// shift focus whether expanded or not
2025-10-22 08:49:49 -04:00
this._focusTextElement(this.fieldset);
};
CollapsibleCheckboxes.prototype.collapse = function(e) {
if (e !== undefined) { e.preventDefault(); }
if (this.expanded) {
2025-10-22 08:49:49 -04:00
this.fieldset.style.display = 'none';
this.expanded = false;
this.summary.update(this.getSelection());
this.footer.update(this.expanded);
2025-10-22 08:49:49 -04:00
if (this.toggleAllButton) {
this.toggleAllButton.parentElement.style.display = 'none';
}
}
// shift focus whether expanded or not
2025-10-22 08:49:49 -04:00
this._focusTextElement(this.summary.text);
};
CollapsibleCheckboxes.prototype.handleClick = function(e) {
if (this.expanded) {
this.collapse(e);
} else {
this.expand(e);
}
};
CollapsibleCheckboxes.prototype.handleSelection = function(e) {
this.summary.update(this.getSelection(), this.total, this.fieldLabel);
this.updateToggleButtonText();
};
CollapsibleCheckboxes.prototype.bindEvents = function() {
2025-10-22 08:49:49 -04:00
this.formGroup.addEventListener('click', (e) => {
if (e.target.closest('.usa-button')) {
this.handleClick.call(this, e);
}
});
this.checkboxes.forEach(cb => {
cb.addEventListener('click', this.handleSelection.bind(this));
});
this.summary.bindEvents(this);
};
Removed all govuk css (#2814) * Removed all govuk css * Updated reference files * Removing govuk js * Fixed casing for modules, removed unused page * Got more reference images * Updated template page * Removed govuk padding util * Updated hint to uswds hint * More govuk cleanup * Commiting backstopjs ref files * Fixed all unit tests that broke due to brittleness around govuk styling * Added new ref images * Final removal of govuk * Officially removed all govuk references * Updated reference file * Updated link to button * UI modernization * Cleanup * removed govuk escaping tests since they are no longer needed * Fix CodeQL security issue in escapeElementName function - Escape backslashes first before other special characters - Prevents potential double-escaping vulnerability - Addresses CodeQL alert about improper string escaping * Found more govuk removal. Fixed unit tests * Add missing pipeline check to pre-commit * updated test * Updated e2e test * More update to e2e test * Fixed another e2e test * Simple PR comments addressed * More updates * Updated backstop ref files * Refactored folder selection for non-admins * Updated redundant line * Updated tests to include correct mocks * Added more ref files * Addressing carlos comments * Addressing Carlo comments, cleanup of window initing * More cleanup and addressing carlo comments * Fixing a11 scan * Fixed a few issues with javascript * Fixed for pr * Fixing e2e tests * Tweaking e2e test * Added more ref files and cleaned up urls.js * Fixed bug with creating new template * Removed brittle test - addressed code ql comment * e2e race condition fix * More e2e test fixes * Updated e2e tests to not wait for text sent * Updated test to not wait for button click response * Made tear down more resilent if staging is down * reverted e2e test to what was working before main merge * Updated backstopRef images * Updated gulp to include job-polling differently
2025-10-06 09:38:54 -04:00
NotifyModules['collapsible-checkboxes'] = function() {
return new CollapsibleCheckboxes();
};
}(window));