// SPDX-FileCopyrightText: 2025-2026 Alpin Insight Solutions GmbH & Co. KG // SPDX-License-Identifier: AGPL-3.0-only /** * Checkbox group component for Insight UI. * * Manages a group of checkboxes with configurable minimum and maximum * selection constraints. Automatically enforces these constraints when * checkboxes are toggled. * * @example * // HTML structure *
* * *
*/ export class Checkbox { /** @type {WeakMap} Weak references to prevent multiple initialization */ static instances = new WeakMap(); /** * Creates a new Checkbox group instance. * * @param {HTMLElement} element - The container element with data-insight-checkbox-group attribute */ constructor(element) { // If an instance for this element already exists, return it if (Checkbox.instances.has(element)) { return Checkbox.instances.get(element); } this.element = element; this.checkboxes = element.getElementsByTagName('input'); this.minChecked = parseInt(element.dataset.minimumChecked, 10) || 0; this.maxChecked = parseInt(element.dataset.maximumChecked, 10) || Infinity; // Live region for screen reader announcements this.announcer = element.querySelector('[data-checkbox-announcement]'); this.msgMinReached = element.dataset.msgMinReached || 'Minimum selection reached.'; this.msgMaxReached = element.dataset.msgMaxReached || 'Maximum selection reached.'; // Store bound handlers for cleanup this.boundChangeHandlers = []; this.init(); this.bindEvents(); this.element.__insightInstance = this; Checkbox.instances.set(element, this); debugLog("New checkbox group created: ", this.element); } /** * Initializes the checkbox group by enforcing min/max constraints. * Automatically checks or unchecks boxes to meet the configured limits. */ init() { const checkedCount = [...this.checkboxes].filter(b => b.checked).length; if (checkedCount < this.minChecked) { // Too few checkboxes are selected, so select the first missing ones. let missingCount = this.minChecked - checkedCount; let checkboxesToCheck = [...this.checkboxes].filter(b => !b.checked); for (let i = 0; i < missingCount; i++) { checkboxesToCheck[i].checked = true; } } else if (checkedCount > this.maxChecked) { // Too many checkboxes are selected, so deselect the last few. let excessCount = checkedCount - this.maxChecked; let checkboxesToUncheck = [...this.checkboxes].filter(b => b.checked); for (let i = 0; i < excessCount; i++) { checkboxesToUncheck[checkboxesToUncheck.length - 1 - i].checked = false; } } } /** * Binds change event listeners to all checkboxes in the group. * Prevents selections that would violate min/max constraints and announces violations. */ bindEvents() { for (let box of this.checkboxes) { const handler = () => { const checkedCount = [...this.checkboxes].filter(b => b.checked).length; if (checkedCount < this.minChecked) { box.checked = true; this.announce(this.msgMinReached); } else if (checkedCount > this.maxChecked) { box.checked = false; this.announce(this.msgMaxReached); } }; this.boundChangeHandlers.push({ element: box, handler }); box.addEventListener('change', handler); } } /** * Announces a message to screen readers via the live region. * * @param {string} message - The message to announce */ announce(message) { if (!this.announcer) return; // Clear and re-set to ensure announcement even if same message this.announcer.textContent = ''; // Use setTimeout to ensure the clearing is processed first setTimeout(() => { this.announcer.textContent = message; }, 50); } /** * Destroys the checkbox group instance and removes all event listeners. * Call this before removing the element from DOM. */ destroy() { debugLog("Destroy checkbox group: ", this.element); this.boundChangeHandlers.forEach(({ element, handler }) => { element.removeEventListener('change', handler); }); this.boundChangeHandlers = []; Checkbox.instances.delete(this.element); delete this.element.__insightInstance; this.element = null; } /** * Initializes all checkbox group instances on the page. * * @static */ static initAll() { document.querySelectorAll('[data-insight-checkbox-group]').forEach(el => new Checkbox(el)); } }