PageSourceSearch

https://www.eventeny.com/js/Components/AccordionCheckbox.js?v=1790189618

js eventeny.com collected 2026-09-24 08:54:17 UTC 18,858 bytes, 625 lines download raw bytes

1/**
2 * @file AccordionCheckbox.js
3 * @description A custom web component that displays an accordion with grouped checkboxes.
4 *
5 * @class AccordionCheckboxElement
6 * @extends HTMLElement
7 *
8 * @example
9 * <accordion-checkbox-element 
10 *   name="export_columns"
11 *   show-select-all="true">
12 * </accordion-checkbox-element>
13 * 
14 * // JavaScript to set data
15 * const accordion = document.querySelector('accordion-checkbox-element');
16 * accordion.setData({
17 *   'Attendee information': [
18 *     { value: 'first_name', label: 'First Name' },
19 *     { value: 'last_name', label: 'Last Name' }
20 *   ],
21 *   'Ticket details': [
22 *     { value: 'ticket_name', label: 'Ticket Name' },
23 *     { value: 'seat', label: 'Seat #' }
24 *   ]
25 * });
26 *
27 * @attribute {string} name - The name attribute for checkboxes (e.g., "export_columns[]")
28 * @attribute {boolean} show-select-all - Whether to show the global "Select All" option
29 * @attribute {string} columns - Number of columns in the grid (default: 3)
30 * 
31 * @method setData - Sets the accordion data structure
32 * @method getData - Returns an array of selected checkbox values
33 * @method reset - Unchecks all checkboxes
34 */
35class AccordionCheckboxElement extends HTMLElement {
36  constructor() {
37    super();
38    this.attachShadow({ mode: 'open' });
39    
40    // Data structure
41    this.sections = {};
42    
43    // Create container
44    this.container = document.createElement('div');
45    this.container.classList.add('accordion-container');
46    
47    // Apply styles
48    const style = document.createElement('style');
49    style.textContent = `
50      .accordion-container {
51        font-family: 'Plus Jakarta Sans', 'Roboto', sans-serif;
52      }
53      
54      .global-select-all {
55        gap: 8px;
56        margin-bottom: 16px;
57        display: flex;
58        align-items: center;
59        cursor: pointer;
60      }
61      
62      .section {
63        margin-bottom: 12px;
64        overflow: hidden;
65        background: white;
66      }
67      
68      .section-header {
69        cursor: pointer;
70        gap: 8px;
71        display: flex;
72        align-items: center;
73        user-select: none;
74        transition: background 0.2s;
75      }
76
77      /* Visible focus ring for keyboard users when toggling accordion sections */
78      .section-header:focus-visible {
79        outline: 2px solid #0389A0;
80        outline-offset: 2px;
81        border-radius: 4px;
82        box-shadow: 0 0 0 4px rgba(3, 137, 160, 0.2);
83      }
84      
85      .section-header-left {
86        display: flex;
87        align-items: center;
88      }
89      
90      .section-title {
91        font-weight: 400;
92        font-size: 14px;
93        color: #333;
94      }
95      
96      .toggle-icon {
97        position: relative;
98        width: 16px;
99        height: 16px;
100        display: inline-block;
101        transition: transform 0.3s ease;
102      }
103      
104      .toggle-icon::before {
105        content: '';
106        position: absolute;
107        left: 50%;
108        top: 50%;
109        transform: translate(-50%, -50%) rotate(45deg);
110        width: 6px;
111        height: 6px;
112        border-right: 2px solid #3F4242;
113        border-bottom: 2px solid #3F4242;
114      }
115      
116      .toggle-icon.expanded {
117        transform: rotate(180deg);
118      }
119      
120      .section-content {
121        max-height: 0;
122        overflow: hidden;
123        transition: max-height 0.3s ease-out;
124      }
125      
126      .section-content.show {
127        max-height: 1000px;
128        overflow-y: auto;
129        transition: max-height 0.5s ease-in;
130      }
131      
132      .section-content-inner {
133        padding: 16px;
134      }
135      
136      .checkbox-grid {
137        display: grid;
138        grid-template-columns: repeat(var(--columns, 3), 1fr);
139        gap: 12px;
140      }
141
142      .checkbox-item.special-checkbox-item {
143        grid-column: 1 / -1;
144        display: block;
145        flex-direction: column;
146        gap: 4px;
147      }
148      
149      .special-checkbox-description {
150        font-size: 12px;
151        color: #666;
152        line-height: 1.4;
153        margin-left: 30px;
154      }
155
156      .checkbox-grid:has(.special-checkbox-item) {
157        grid-template-columns: 1fr;
158      }
159      
160      @media (max-width: 768px) {
161        .checkbox-grid {
162          grid-template-columns: 1fr;
163        }
164      }
165      
166      .checkbox-item {
167        display: flex;
168        align-items: flex-start;
169        gap: 8px;
170        cursor: pointer;
171      }
172      
173      .checkbox-input {
174        cursor: pointer;
175        width: 100%;
176      }
177      
178
179
180
181
182    `;
183    
184    this.shadowRoot.appendChild(style);
185    this.shadowRoot.appendChild(this.container);
186  }
187  
188  connectedCallback() {
189    this.columns = this.getAttribute('columns') || '3';
190    this.container.style.setProperty('--columns', this.columns);
191    this.render();
192  }
193  
194  static get observedAttributes() {
195    return ['name', 'show-select-all', 'columns', 'default-open'];
196  }
197  
198  attributeChangedCallback(name, oldValue, newValue) {
199    if (name === 'columns' && newValue !== oldValue) {
200      this.columns = newValue;
201      this.container.style.setProperty('--columns', this.columns);
202    }
203  }
204  
205  setData(data, selectAll = false) {
206    this.sections = data;
207    this.render();
208
209    if (selectAll) {
210      setTimeout(() => {
211        this.handleGlobalSelectAll(true);
212      }, 0);
213    }
214  }
215  
216  render() {
217    const name = this.getAttribute('name') || 'checkbox';
218    const showSelectAll = this.hasAttribute('show-select-all');
219    
220    this.container.innerHTML = '';
221    
222    // Global select all
223    if (showSelectAll) {
224      const globalSelectAll = this.createGlobalSelectAll();
225      this.container.appendChild(globalSelectAll);
226    }
227    
228    // Render sections
229    Object.entries(this.sections).forEach(([sectionTitle, items]) => {
230      const section = this.createSection(sectionTitle, items, name);
231      this.container.appendChild(section);
232    });
233  }
234  
235  createGlobalSelectAll() {
236    const wrapper = document.createElement('div');
237    wrapper.classList.add('global-select-all');
238    
239    const checkbox = document.createElement('checkbox-element');
240    checkbox.classList.add('global-checkbox');
241    checkbox.setAttribute('label', 'Select all');
242    checkbox.style.setProperty('--label-font-size', '14px');
243    checkbox.style.setProperty('--label-font-weight', '500');
244    
245    checkbox.addEventListener('change', () => {
246      this.handleGlobalSelectAll(checkbox.isChecked());
247    });
248    
249    wrapper.appendChild(checkbox);
250    
251    return wrapper;
252  }
253  
254  createSection(title, items, name) {
255    const section = document.createElement('div');
256    section.classList.add('section');
257    
258    // Header
259    const header = document.createElement('div');
260    header.classList.add('section-header');
261    // Keyboard accessibility: header is a toggle button reachable via Tab.
262    // The inner checkbox-element remains independently focusable.
263    header.setAttribute('tabindex', '0');
264    header.setAttribute('role', 'button');
265    header.setAttribute('aria-expanded', this.hasAttribute('default-open') ? 'true' : 'false');
266
267    const headerLeft = document.createElement('div');
268    headerLeft.classList.add('section-header-left');
269    
270    // Section checkbox
271    const sectionCheckbox = document.createElement('checkbox-element');
272    sectionCheckbox.classList.add('section-checkbox');
273    sectionCheckbox.dataset.section = title;
274    sectionCheckbox.setAttribute('label', '');
275    
276    sectionCheckbox.addEventListener('change', (e) => {
277      e.stopPropagation();
278      this.handleSectionSelectAll(title, sectionCheckbox.isChecked());
279    });
280    
281    const sectionTitle = document.createElement('span');
282    sectionTitle.classList.add('section-title');
283    sectionTitle.textContent = title;
284    
285    headerLeft.appendChild(sectionCheckbox);
286    headerLeft.appendChild(sectionTitle);
287    
288    // Toggle icon
289    const toggleIcon = document.createElement('span');
290    toggleIcon.classList.add('toggle-icon');
291    
292    header.appendChild(headerLeft);
293    header.appendChild(toggleIcon);
294    
295    // Content
296    const content = document.createElement('div');
297    content.classList.add('section-content');
298    if (this.hasAttribute('default-open')) {
299      content.classList.add('show');
300      toggleIcon.classList.add('expanded');
301    }
302    
303    const contentInner = document.createElement('div');
304    contentInner.classList.add('section-content-inner');
305    
306    const grid = document.createElement('div');
307    grid.classList.add('checkbox-grid');
308    
309    // Create checkboxes
310    items.forEach(item => {
311      const checkboxItem = this.createCheckboxItem(item, name, title);
312      grid.appendChild(checkboxItem);
313    });
314    
315    contentInner.appendChild(grid);
316    content.appendChild(contentInner);
317    
318    section.appendChild(header);
319    section.appendChild(content);
320    
321    // Toggle expand/collapse
322    const toggleSection = () => {
323      const willShow = !content.classList.contains('show');
324      content.classList.toggle('show', willShow);
325      header.classList.toggle('expanded', willShow);
326      toggleIcon.classList.toggle('expanded', willShow);
327      header.setAttribute('aria-expanded', willShow ? 'true' : 'false');
328    };
329
330    header.addEventListener('click', (e) => {
331      if (e.target.tagName.toLowerCase() === 'checkbox-element') return;
332      toggleSection();
333    });
334
335    // Keyboard: Enter/Space on the header toggles the section, mirroring click
336    header.addEventListener('keydown', (e) => {
337      if (e.target.tagName.toLowerCase() === 'checkbox-element') return;
338      if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
339        e.preventDefault();
340        toggleSection();
341      }
342    });
343
344    return section;
345  }
346  
347  createCheckboxItem(item, name, section) {
348      const wrapper = document.createElement('div');
349      wrapper.classList.add('checkbox-item');
350      
351      if (item.special) wrapper.classList.add('special-checkbox-item'); // Add special class if needed
352      
353      const checkbox = document.createElement('checkbox-element');
354      checkbox.classList.add('checkbox-input');
355      checkbox.setAttribute('label', item.label);
356      checkbox.setAttribute('value', item.value);
357      checkbox.dataset.section = section;
358
359      // Add data attributes
360      if (item.dataAttributes) {
361          Object.entries(item.dataAttributes).forEach(([key, value]) => {
362              checkbox.dataset[key] = value;
363          });
364      }
365      
366      // Mark as special checkbox
367      if (item.special) checkbox.dataset.special = 'true';
368
369      checkbox.style.setProperty('--label-font-size', '14px');
370      checkbox.style.setProperty('--label-font-weight', '500');
371      
372      if (item.checked) {
373          checkbox.setAttribute('checked', '');
374      }
375      
376      checkbox.addEventListener('change', () => {
377          // Handle special checkbox separately
378          if (item.special) this.handleMergeCheckboxChange(item.value, checkbox.isChecked());
379          
380          this.updateSectionCheckbox(section);
381          this.updateGlobalCheckbox();
382          this.syncToForm();
383          this.dispatchChangeEvent();
384      });
385      
386      wrapper.appendChild(checkbox);
387      
388      // Add description text for special checkboxes
389      if (item.special && item.description) {
390          const description = document.createElement('div');
391          description.classList.add('special-checkbox-description');
392          description.textContent = item.description;
393          wrapper.appendChild(description);
394      }
395      
396      return wrapper;
397  }
398
399  handleMergeCheckboxChange(mergeValue, isChecked) {
400      const form = this.closest('form');
401      if (!form) return;
402
403      const inputName = `export[${mergeValue}]`;
404
405      // Use find() instead of querySelector to avoid CSS selector issues with brackets in attribute values
406      const existing = Array.from(form.querySelectorAll('input[type="hidden"]')).find(el => el.name === inputName);
407      if (existing) existing.remove();
408
409      if (isChecked) {
410          const input = document.createElement('input');
411          input.type = 'hidden';
412          input.name = inputName;
413          input.value = '1';
414          input.dataset.mergeOption = 'true';
415          form.appendChild(input);
416      }
417  }
418
419  syncToForm() {
420    const form = this.closest('form');
421    if (!form) return;
422
423    const name = this.getAttribute('name') || 'checkbox';
424
425    const oldInputs = form.querySelectorAll(`input[data-accordion-sync="${name}"]`);
426    oldInputs.forEach(input => input.remove());
427
428    const checkedBoxes = this.shadowRoot.querySelectorAll('.checkbox-input');
429    checkedBoxes.forEach(cb => {
430      if (cb.dataset.special === 'true') {
431        this.handleMergeCheckboxChange(cb.getAttribute('value'), cb.isChecked());
432        return;
433      }
434      if (!cb.isChecked()) return;
435      const hiddenInput = document.createElement('input');
436      hiddenInput.type = 'hidden';
437      hiddenInput.name = name + '[]';
438      hiddenInput.value = cb.getAttribute('value');
439      hiddenInput.dataset.accordionSync = name;
440      form.appendChild(hiddenInput);
441    });
442  }
443
444  handleGlobalSelectAll(checked) {
445    const allCheckboxes = this.shadowRoot.querySelectorAll('.checkbox-input');
446    allCheckboxes.forEach(cb => {
447        if (cb.dataset.special !== 'true') {
448            cb.checked = checked;
449        } else {
450            cb.checked = checked;
451            this.handleMergeCheckboxChange(cb.getAttribute('value'), checked);
452        }
453    });
454    
455    const allSectionCheckboxes = this.shadowRoot.querySelectorAll('.section-checkbox');
456    allSectionCheckboxes.forEach(cb => {
457      cb.checked = checked;
458    });
459
460    const globalCheckbox = this.shadowRoot.querySelector('.global-checkbox');
461    if (globalCheckbox) {
462      globalCheckbox.checked = checked;
463      globalCheckbox.removeAttribute('indeterminate');
464    }
465
466    this.syncToForm();
467    this.dispatchChangeEvent();
468  }
469  
470  handleSectionSelectAll(section, checked) {
471    const sectionCheckboxes = this.shadowRoot.querySelectorAll(`.checkbox-input[data-section="${section}"]`);
472    sectionCheckboxes.forEach(cb => {
473        if (cb.dataset.special !== 'true') {
474            cb.checked = checked;
475        } else {
476            cb.checked = checked;
477            this.handleMergeCheckboxChange(cb.getAttribute('value'), checked);
478        }
479    });
480
481    // Remove indeterminate state when explicitly checking/unchecking
482    const sectionCheckbox = this.shadowRoot.querySelector(`.section-checkbox[data-section="${section}"]`);
483    if (sectionCheckbox) {
484      sectionCheckbox.removeAttribute('indeterminate');
485    }
486
487    this.syncToForm();
488    this.updateGlobalCheckbox();
489    this.dispatchChangeEvent();
490  }
491  
492  updateSectionCheckbox(section) {
493    const sectionCheckboxes = Array.from(
494        this.shadowRoot.querySelectorAll(`.checkbox-input[data-section="${section}"]`)
495    ).filter(cb => cb.dataset.special !== 'true');
496
497    const sectionCheckbox = this.shadowRoot.querySelector(`.section-checkbox[data-section="${section}"]`);
498    
499    if (sectionCheckbox) {
500      const checkedCount = sectionCheckboxes.filter(cb => cb.isChecked()).length;
501      const totalCount = sectionCheckboxes.length;
502      
503      if (checkedCount === 0) {
504        // no selection under it - uncheck and remove indeterminate
505        sectionCheckbox.checked = false;
506        sectionCheckbox.removeAttribute('indeterminate');
507      } else if (checkedCount === totalCount) {
508        // all selected - check and remove indeterminate
509        sectionCheckbox.checked = true;
510        sectionCheckbox.removeAttribute('indeterminate');
511      } else {
512        // partially selected - set indeterminate state
513        sectionCheckbox.checked = false;
514        sectionCheckbox.setAttribute('indeterminate', '');
515      }
516    }
517  }
518  
519  updateGlobalCheckbox() {
520    const globalCheckbox = this.shadowRoot.querySelector('.global-checkbox');
521    if (!globalCheckbox) return;
522    
523    const allCheckboxes = Array.from(
524        this.shadowRoot.querySelectorAll('.checkbox-input')
525    ).filter(cb => cb.dataset.special !== 'true');
526
527    if (allCheckboxes.length === 0) return;
528    
529    const checkedCount = allCheckboxes.filter(cb => cb.isChecked()).length;
530    const totalCount = allCheckboxes.length;
531    
532    if (checkedCount === 0) {
533      // no selection
534      globalCheckbox.checked = false;
535      globalCheckbox.removeAttribute('indeterminate');
536    } else if (checkedCount === totalCount) {
537      // all selected
538      globalCheckbox.checked = true;
539      globalCheckbox.removeAttribute('indeterminate');
540    } else {
541      // partially selected
542      globalCheckbox.checked = false;
543      globalCheckbox.setAttribute('indeterminate', '');
544    }
545  }
546  
547  dispatchChangeEvent() {
548    this.dispatchEvent(new CustomEvent('change', {
549      detail: { values: this.getData() },
550      bubbles: true,
551      composed: true
552    }));
553  }
554
555
556  filterByApps(selectedAppIds) {
557    const allQuestions = this.shadowRoot.querySelectorAll('.checkbox-input');
558    
559    allQuestions.forEach(checkbox => {
560      const appId = checkbox.dataset.app;
561      const checkboxItem = checkbox.closest('.checkbox-item');
562      
563      if (appId) {
564        if (selectedAppIds.includes(appId)) {
565          checkboxItem.style.display = '';
566          checkbox.removeAttribute('disabled');
567          checkbox.checked = true;
568        } else {
569          checkboxItem.style.display = 'none';
570          checkbox.checked = false;
571          checkbox.setAttribute('disabled', '');
572        }
573      }
574    });
575
576    // update section visibility
577    Object.keys(this.sections).forEach(sectionTitle => {
578      this.updateSectionVisibility(sectionTitle);
579    });
580
581    this.syncToForm();
582    this.updateGlobalCheckbox();
583  }
584
585  updateSectionVisibility(sectionTitle) {
586    const section = Array.from(this.shadowRoot.querySelectorAll('.section'))
587      .find(s => s.querySelector('.section-title').textContent === sectionTitle);
588    
589    if (!section) return;
590
591    const visibleItems = Array.from(
592      section.querySelectorAll('.checkbox-item')
593    ).filter(item => item.style.display !== 'none');
594
595    section.style.display = visibleItems.length === 0 ? 'none' : '';
596    
597    if (visibleItems.length > 0) {
598      this.updateSectionCheckbox(sectionTitle);
599    }
600  }
601  
602  getData() {
603    const checkedBoxes = this.shadowRoot.querySelectorAll('.checkbox-input');
604    return Array.from(checkedBoxes)
605      .filter(cb => cb.isChecked())
606      .map(cb => cb.getAttribute('value'));
607  }
608  
609  reset() {
610    const allCheckboxes = this.shadowRoot.querySelectorAll('checkbox-element');
611    allCheckboxes.forEach(cb => {
612      cb.checked = false;
613    });
614    this.dispatchChangeEvent();
615  }
616  
617  isDisabled() {
618    return this.hasAttribute('disabled');
619  }
620}
621
622// Define the custom element
623if (!customElements.get('accordion-checkbox-element')) {
624  customElements.define('accordion-checkbox-element', AccordionCheckboxElement);
625}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.