PageSourceSearch

https://www.eventeny.com/js/Components/BasicCheckbox.js?v=1790795785

js eventeny.com collected 2026-10-01 10:25:21 UTC 24,081 bytes, 643 lines download raw bytes

1/**
2 * @file BasicCheckbox.js
3 * @description This is a custom web component that represents a simple checkbox or radio button with customizable text.
4 *
5 * @class CheckboxElement
6 * @extends HTMLElement
7 *
8 * @classdesc A custom HTML element that provides a checkbox or radio button interface with customizable text.
9 * This component features a user-friendly UI and supports dynamic changes to the checkbox or radio button state.
10 *
11 * @example
12 * <checkbox-element label="I agree to the terms and conditions"></checkbox-element>
13 * 
14 * @example
15 * <div class="radio-buttons-group">
16 *   <checkbox-element label="Option 1" radio name="group1"></checkbox-element>
17 *   <checkbox-element label="Option 2" radio name="group1" checked></checkbox-element>
18 *   <checkbox-element label="Option 3" radio name="group1"></checkbox-element>
19 * </div>
20 *
21 * @attribute {string} label - The text displayed next to the checkbox or radio button.
22 * @attribute {boolean} checked - If present, the checkbox or radio button is checked.
23 * @attribute {boolean} disabled - If present, the checkbox or radio button is disabled.
24 * @attribute {boolean} radio - If present, the element behaves like a radio button, allowing only one selection per group.
25 * @attribute {string} name - Attribute to group radio buttons together when creating them.
26 * @attribute {string} value - Must be used in combination with the "form-checkbox" class in order to send form data
27 * @attribute {boolean} error - Puts the checkbox into error state (red box + optional message)
28 * @attribute {string} error-message - Message shown under the checkbox when `error` is set
29 * @attribute {boolean} disable-select-all
30 * @attribute {boolean} max-content - If present, the checkbox container width will be changed from "100%" to "max-content".
31 *
32 * @cssprop --label-font-size - font size of the label text, default 1rem
33 * @cssprop --label-color - text/fill color of the label text, default #484848
34 * @cssprop --label-line-height - line height of the label text, default 1.5
35 * Disables the built-in automatic select-all synchronization.
36 * Use this when selection state is managed by a parent component (e.g. tables with pagination, virtualized rows, or bulk actions).
37 * Required when selection is managed by a parent component.
38 * Tables often re-render rows (replace DOM), making checkbox-level DOM queries
39 * for auto select-all unreliable. Parent should orchestrate selection.
40 *
41 * @method toggleCheck - Toggles the checked state of the checkbox or radio button.
42 * @method handleChange - Handles changes to the checkbox or radio button state and dispatches a 'change' event.
43 * @method handleRadio - Handles radio button behavior by deselecting other radio buttons in the group.
44 * @method isChecked - Returns the checked state of the checkbox or radio button.
45 * @method setError(message) - Puts the checkbox into error state with an optional message
46 * @method clearError() - Clears error state and hides the error message
47 *
48 * @event change - Dispatched when the checked state of the checkbox or radio button changes.
49 */
50import { WebComponent } from '../../../js/Components/Base/WebComponent.js';
51
52// Static markup: same structure as before, just parsed once per class (via
53// WebComponent.getTemplateClone) instead of built with createElement calls on every
54// instance. display:flex/align-items:flex-start are already set by the .checkbox-container
55// rule below, so the inline styles previously set in JS were redundant and are dropped.
56const TEMPLATE_HTML = `
57    <label class="checkbox-container">
58        <input type="checkbox" class="checkbox-input">
59        <span class="custom-checkbox"></span>
60        <span class="checkbox-label ml-half huge text-dark" part="checkbox-label"></span>
61    </label>
62    <div class="checkbox-error-message" aria-live="polite"></div>
63`;
64
65const COMPONENT_CSS = `
66      :host {
67        user-select: none;
68        -webkit-user-select: none;
69      }
70      .checkbox-container {
71        cursor: pointer;
72        user-select: none;
73        display: flex;
74        align-items: var(--checkbox-align, flex-start);
75        gap: 8px;
76        width: 100%;
77      }
78      /* Label-less checkbox (table select-all, row select): with 
78nothing to
79         sit beside the box, the gap and the empty label span still widened the
80         row, so every ring drawn around it -- hover and keyboard focus alike --
81         enclosed that dead space instead of the box. */
82      .checkbox-container.no-label {
83        gap: 0;
84        width: max-content;
85      }
86      .checkbox-container.no-label .checkbox-label { display: none; }
87      .checkbox-input {
88        position: absolute;
89        opacity: 0;
90        width: 0;
91        height: 0;
92      }
93      .custom-checkbox {
94        width: 20px;
95        height: 20px;
96        background-color: #fff;
97        border: 1px solid #ccc;
98        border-radius: 4px;
99        display: inline-block;
100        position: relative;
101        flex-shrink: 0;
102      }
103
104      /* ============================= */
105      /* Hover states (box only)       */
106      /* ============================= */
107
108      /* Hover — unchecked + enabled Visual: dark border, white background. */
109      .checkbox-input:not(:checked):not(:indeterminate):not(:disabled) + .custom-checkbox:hover {
110        background-color: #fff;
111        border: 1px solid #3F4242;
112      }
113
114      /* Hover — checked + enabled Visual: lighter green fill plus a darker
115         border. The fill shift alone reads too close to the checked color
116         (#038580 vs #08A6A0), so the host-level halo (below) does the heavy
117         lifting and the border change reinforces the box itself. */
118      .checkbox-input:checked:not(:disabled) + .custom-checkbox:hover {
119        background-color: #08A6A0;
120        border: 1px solid #026E6A;
121      }
122
123      /* Hover — indeterminate + enabled Visual: matches checked hover. */
124      .checkbox-input:indeterminate:not(:disabled) + .custom-checkbox:hover {
125        background-color: #08A6A0;
126        border: 1px solid #026E6A;
127      }
128
129      /* ============================= */
130      /* Hover ring around full row    */
131      /* ============================= */
132
133      /* Hover halo wraps the checkbox AND its label so the affordance is
134         obvious — hovering anywhere on the row lights up an outline around
135         the whole item. :has() narrows by state so disabled boxes never get
136         a ring. */
137      :host(:has(.checkbox-input:not(:checked):not(:indeterminate):not(:disabled))):hover {
138        outline: 1.5px solid #3F4242;
139        outline-offset: 3px;
140        border-radius: 4px;
141        box-shadow: 0 0 0 4px rgba(63, 66, 66, 0.15);
142      }
143      :host(:has(.checkbox-input:checked:not(:disabled))):hover,
144      :host(:has(.checkbox-input:indeterminate:not(:disabled))):hover {
145        outline: 1.5px solid #026E6A;
146        outline-offset: 3px;
147        border-radius: 4px;
148        box-shadow: 0 0 0 4px rgba(3, 133, 128, 0.25);
149      }
150
151      /* Hover — disabled (safety override) Visual: no hover effect */
152      .checkbox-input:disabled + .custom-checkbox:hover {
153        background-color: #D8DBDB;
154        border: 1px solid #D8DBDB;
155      }
156
157      /* ============================= */
158      /* Selected states (box fill)    */
159      /* ============================= */
160
161      /* Checked OR indeterminate (enabled or disabled) Visual: dark green fill */
162      .checkbox-input:checked + .custom-checkbox, .checkbox-input:indeterminate + .custom-checkbox{
163        background-color: #038580;
164        border: 1px solid #038580;
165      }
166
167      /* ============================= */
168      /* Check mark                    */
169      /* ============================= */
170
171      /* Checked — enabled Visual: white check mark */
172      .checkbox-input:checked + .custom-checkbox::after {
173        content: '';
174        position: absolute;
175        top: 3px;
176        left: 7px;
177        width: 5px;
178        height: 10px;
179        border: solid white;
180        border-width: 0 2px 2px 0;
181        transform: rotate(45deg);
182        z-index: 1;
183      }
184
185      /* Checked — disabled Visual: gray check mark */
186      .checkbox-input:checked:disabled + .custom-checkbox::after {
187        border-color: #9DA6A5;
188      }
189
190
191
192      /* ============================= */
193      /* Indeterminate mark            */
194      /* ============================= */
195
196      /* Indeterminate — enabled Visual: white horizontal dash */
197      .checkbox-input:indeterminate + .custom-checkbox::after {
198        content: '';
199        position: absolute;
200        top: 9px;
201        left: 4px;
202        width: 12px;
203        height: 2px;
204        background: white;
205        border-radius: 2px;
206        z-index: 1;
207      }
208
209      /* Indeterminate — disabled Visual: gray horizontal dash */
210      .checkbox-input:indeterminate:disabled + .custom-checkbox::after {
211        border: 1px solid #9DA6A5;
212        background: #9DA6A5;
213      }
214
215      /* ============================= */
216      /* Disabled state (box)          */
217      /* ============================= */
218
219      /* Disabled checkbox Visual: gray fill, gray border, no interaction */
220      .checkbox-input:disabled + .custom-checkbox {
221        background-color: #D8DBDB;
222        border: 1px solid #D8DBDB;
223        cursor: not-allowed;
224      }
225
226      /* ============================= */
227      /* Error state (box + message)   */
228      /* ============================= */
229
230      /* Error — unchecked Visual: red border, white background */
231      :host([error]) .checkbox-input:not(:checked):not(:indeterminate):not(:disabled) + .custom-checkbox {
232        background-color: #fff;
233        border: 1.5px solid #D63952;
234      }
235
236      /* Error — checked/indeterminate Visual: red border stays visible around the filled box */
237      :host([error]) .checkbox-input:checked:not(:disabled) + .custom-checkbox,
238      :host([error]) .checkbox-input:indeterminate:not(:disabled) + .custom-checkbox {
239        border: 1.5px solid #D63952;
240        box-shadow: 0 0 0 1px #D63952;
241      }
242
243      .checkbox-error-message {
244        color: #D63952;
245        font-size: 13px;
246        line-height: 1.4;
247        margin-top: 4px;
248      }
249
250      /* ============================= */
251      /* Keyboard focus indicator      */
252      /* ============================= */
253
254      /* Ring wraps the entire row (checkbox + label) when focus arrives via
255         Tab/arrow keys. Outline goes on the inner .checkbox-container (a
256         flex block with defined dimensions) rather than the host — the host
257         is display: inline by default, so a host-level outline draws at the
258         inline content's bounding box and produces stray fragments. The
259         keyboard-focused class is added from JS because :focus-visible inside
260         :has() doesn't reactively update on the host across all Chromium
261         versions. Color stays brand teal for UI consistency. */
262      :host(.keyboard-focused) .checkbox-container {
263        outline: 2px solid #0389A0;
264        outline-offset: 3px;
265        border-radius: 4px;
266        box-shadow: 0 0 0 4px rgba(3, 137, 160, 0.2);
267      }
268      .checkbox-input:focus-visible + .custom-checkbox {
269        border-color: #0389A0;
270      }
271
272      /* ============================= */
273      /* Label text                    */
274      /* ============================= */
275
276      /* Checkbox label text */
277      .checkbox-label {
278        font-family: 'Plus Jakarta Sans', 'Roboto', sans-serif;
279        font-size: var(--label-font-size, 1rem);
280        font-weight: 400;
281        color: var(--label-color, #484848);
282        line-height: var(--label-line-height, 1.5);
283        fill: var(--label-color, #484848);
284        min-width: 0;
285        white-space: normal;
286        overflow-wrap: anywhere;
287        word-break: break-word;
288      }
289    `;
290
291class CheckboxElement extends HTMLElement {
292  constructor() {
293    super();
294    this.attachShadow({ mode: 'open' });
295
296    this.shadowRoot.appendChild(WebComponent.getTemplateClone(CheckboxElement, TEMPLATE_HTML));
297
298    const sheet = WebComponent.getComponentStyleSheet(CheckboxElement, COMPONENT_CSS);
299    if (sheet) {
300      this.shadowRoot.adoptedStyleSheets = [sheet];
301    } else {
302      const style = document.createElement('style');
303      style.textContent = COMPONENT_CSS;
304      this.shadowRoot.appendChild(style);
305    }
306
307    this.container = this.shadowRoot.querySelector('.checkbox-container');
308    this.checkbox = this.shadowRoot.querySelector('.checkbox-input');
309    this.customCheckbox = this.shadowRoot.querySelector('.custom-checkbox');
310    this.labelText = this.shadowRoot.querySelector('.checkbox-label');
311    this.labelText.innerHTML = this.getAttribute('label') || '';
312    this._syncLabelPresence();
313
314    // Error message shown below the row when `error` is set (accessibility.md: error container
315    // adjacent to the input, inside an aria-live region so it's announced without a full reload).
316    // The element itself is static markup (TEMPLATE_HTML); only the id needs to be unique per instance.
317    this._errorId = 'checkbox-error-' + Math.random().toString(36).slice(2);
318    this.errorMessageEl = this.shadowRoot.querySelector('.checkbox-error-message');
319    this.errorMessageEl.id = this._errorId;
320    this.errorMessageEl.style.display = 'none';
321  }
322
323  /**
324   * Called when the element is added to the document's DOM.
325   * Initializes the checkbox or radio button state and adds event listeners.
326   */
327  connectedCallback() {
328    if (this.hasAttribute('checked')) {
329      this.checkbox.checked = true;
330    }
331    if (this.hasAttribute('disabled')) {
332      this.checkbox.disabled = true;
333    }
334    if (this.hasAttribute('indeterminate')) {
335      this.checkbox.indeterminate = true;
336    }
337    if (this.hasAttribute('radio')) {
338      this.checkbox.type = 'radio';
339      // Set a common name to ensure only one radio can be selected
340      if (!this.hasAttribute('name')) {
341        this.checkbox.name = 'default-radio-group';
342      } else {
343        this.checkbox.name = this.getAttribute('name');
344      }
345    }
346
347    //Variation on style for a wider label that takes all available width and not just the max-content
348    let wider = this.getAttribute('wider') ?? false;
349    if(wider){
350      this.container.style.width = "100%";
351      this.labelText.style.width = "100%";
352    }
353
354    // Handle max-content attribute
355    if (this.hasAttribute('max-content')) {
356      this.container.style.width = 'max-content';
357    }
358
359    // Only create hidden input if it has class "form-checkbox"
360    if (
361      this.classList.contains('form-checkbox') &&
362      this.hasAttribute('name') &&
363      this.checkbox.checked
364    ) {
365      this.hiddenInput = document.createElement('input');
366      this.hiddenInput.type = 'hidden';
367      this.hiddenInput.name = this.getAttribute('name');
368      this.hiddenInput.value = this.getAttribute('value') || 'on';
369      this.closest('form')?.appendChild(this.hiddenInput);
370    }
371
372    this.checkbox.addEventListener('change', () => this.handleChange());
373
374    // Enter toggles the checkbox to match Space; native checkbox inputs respond
375    // to Space but not Enter, and keyboard users expect both to activate.
376    this.checkbox.addEventListener('keydown', (e) => {
377      if (e.key === 'Enter' && !this.checkbox.disabled) {
378        e.preventDefault();
379        this.toggleCheck();
380      }
381    });
382
383    // Mirror :focus-visible into a host class so the wrap-around ring fires
384    // reliably (see CSS note above). matches(':focus-visible') tells us focus
385    // arrived via keyboard, not mouse — so clicking the checkbox stays clean.
386    this.checkbox.addEventListener('focus', () => {
387      if (this.checkbox.matches(':focus-visible')) {
388        this.classList.add('keyboard-focused');
389      }
390    });
391    this.checkbox.addEventListener('blur', () => {
392      this.classList.remove('keyboard-focused');
393    });
394  }
395
396  /**
397   * Observed attributes for the CheckboxElement component.
398   * @returns {Array<string>} The list of attribute names to observe.
399   */
400  static get observedAttributes() {
401    return ['label', 'checked', 'disabled', 'indeterminate', 'radio', 'name', 'value', 'max-content', 'error', 'error-message'];
402  }
403
404  /**
405   * Called when an observed attribute has been added, removed, updated, or replaced.
406   * Handles updates to the label, checked, disabled, and radio attributes.
407   * 
408   * @param {string} name - The name of the attribute that changed.
409   * @param {string} oldValue - The old value of the attribute.
410   * @param {string} newValue - The new value of the attribute.
411   */
412  attributeChangedCallback(name, oldValue, newValue) {
413    if (name === 'label' && newValue !== oldValue) {
414      this.labelText.innerHTML = newValue;
415      this._syncLabelPresence();
416    } else if (name === 'checked') {
417      this.checkbox.checked = newValue !== null;
418    } else if (name === 'disabled') {
419      this.checkbox.disabled = newValue !== null;
420    } else if (name === 'indeterminate') {
421      this.checkbox.indeterminate = newValue !== null;
422    } else if (name === 'radio') {
423      this.checkbox.type = newValue !== null ? 'radio' : 'checkbox';
424    } else if (name === 'name') {
425      this.checkbox.name = newValue;
426    } else if (name === 'value') {
427      this.checkbox.value = newValue;
428    } else if (name === 'max-content') {
429      this.container.style.width = newValue !== null ? 'max-content' : '';
430    } else if (name === 'error' || name === 'error-message') {
431      this._updateError();
432    }
433  }
434
435  /** Marks the row label-less so the hover and focus rings hug the box. */
436  _syncLabelPresence() {
437    this.container.classList.toggle('no-label', !this.labelText.innerHTML.trim());
438  }
439
440  /**
441   * Syncs the error box styling, message text and aria-describedby from the
442   * `error` / `error-message` attributes.
443   */
444  _updateError() {
445    const hasError = this.hasAttribute('error');
446    const message = this.getAttribute('error-message') || '';
447    this.errorMessageEl.textContent = message;
448    this.errorMessageEl.style.display = hasError && message ? '' : 'none';
449    if (hasError) {
450      this.checkbox.setAttribute('aria-describedby', this._errorId);
451    } else {
452      this.checkbox.removeAttribute('aria-describedby');
453    }
454  }
455
456  /**
457   * Puts the checkbox into error state (red box) with an optional message shown below it.
458   * @param {string} [message] - Error text; omit for just the red box with no message.
459   */
460  setError(message) {
461    if (message) this.setAttribute('error-message', message);
462    this.setAttribute('error', '');
463  }
464
465  /**
466   * Clears error state and hides the error message.
467   */
468  clearError() {
469    this.removeAttribute('error');
470    this.removeAttribute('error-message');
471  }
472
473  /**
474   * Toggles the checked state of the checkbox or radio button.
475   */
476  toggleCheck(state = null) {
477    // If a specific state is provided, set it; otherwise, toggle
478    if (state !== null) {
479      this.checkbox.checked = state;
480    } else {
481      this.checkbox.checked = !this.checkbox.checked;
482    }
483  
484    this.handleChange();
485  }
486
487  /**
488   * Handles changes to the checkbox or radio button state and dispatches a 'change' event.
489   */
490  handleChange() {
491    // Handle radio button behavior
492    if (this.checkbox.type === 'radio' && this.checkbox.checked) {
493      this.handleRadio();
494    }
495
496    /**
497     * Automatic select-all behavior is disabled when `disable-select-all`
498     * is present.
499     *
500     * This is required when selection state is managed by a parent component
501     * (e.g. a data table handling pagination, bulk actions, or cross-page
502     * selection).
503     */
504    if (!this.hasAttribute('disable-select-all')) {
505      if (this.classList.contains('select-all-option') || this.classList.contains('select-option')) {
506        this.handleSelectAll();
507      }
508    }
509
510    const name = this.getAttribute('name');
511    const value = this.getAttribute('value') || 'on';
512
513    //only append hidden input if checkbox is checked
514    if (this.classList.contains('form-checkbox') && name) {
515      if (this.checkbox.checked) {
516        if (!this.hiddenInput) {
517          this.hiddenInput = document.createElement('input');
518          this.hiddenInput.type = 'hidden';
519          this.hiddenInput.name = name;
520          this.hiddenInput.value = value;
521          this.closest('form')?.appendChild(this.hiddenInput);
522        } else {
523          this.hiddenInput.value = value;
524        }
525      } else {
526        if (this.hiddenInput) {
527          this.hiddenInput.remove();
528          this.hiddenInput = null;
529        }
530      }
531    }
532
533    // Sync the checked attribute with the internal checkbox state
534    if (this.checkbox.checked) {
535      this.setAttribute('checked', '');
536    } else {
537      this.removeAttribute('checked');
538    }
539
540    // Dispatch a 'change' event with the checked state
541    this.dispatchEvent(new CustomEvent('change', {
542      detail: { checked: this.checkbox.checked },
543      bubbles: true
544    }));
545  }
546
547
548  /**
549   * Handles radio button behavior by deselecting other radio buttons in the group.
550   */
551  handleRadio() {
552    const root = this.getRootNode();
553
554    const radios = root.querySelectorAll(`checkbox-element[radio][name="${this.checkbox.name}"]`);
555
556    //Uncheck all other radio buttons in the group
557    radios.forEach((radio) => {
558      if (radio !== this && radio.isChecked()) {
559        radio.toggleCheck();
560      }
561    });
562  }
563
564  /**
565   * Handles the select all option by toggling the checked state of all checkboxes given a source.
566   */
567  handleSelectAll() {
568    const source = this.dataset.source;
569    const root = this.getRootNode();
570
571    if (this.classList.contains('select-all-option')) {
572      const isAllChecked = this.isChecked();
573      const selectOptions = root.querySelectorAll(`checkbox-element.select-option[data-source="${source}"]`);
574
575      selectOptions.forEach((option) => {
576        //Match all checkboxes with the status of the `select-all-option` button
577        if (option.isChecked() !== isAllChecked) {
578          option.toggleCheck();
579        }
580      });
581    } else if (this.classList.contains('select-option')) {
582      //Check if all checkboxes with `select-option` class are checked
583      //If all are checked, check the `select-all-option` button
584      //If part of them are checked, set the `select-all-option` button to indeterminate state
585      //If not all are checked, uncheck the `select-all-option` button
586
587      const selectOptions = root.querySelectorAll(`checkbox-element.select-option[data-source="${source}"]`);
588      const selectAllOption = root.querySelector(`checkbox-element.select-all-option[data-source="${source}"]`);
589
590
591      const checkedCount = Array.from(selectOptions).filter(opt => opt.isChecked()).length;
592      const totalCount = selectOptions.length;
593
594      const allChecked = checkedCount === totalCount && totalCount > 0;
595      const noneChecked = checkedCount === 0;
596      const isIndeterminate = !allChecked && !noneChecked;
597
598      selectAllOption.checkbox.checked = allChecked;
599      selectAllOption.checkbox.indeterminate = isIndeterminate;
600    }
601  }
602
603  /**
604   * Returns the checked state of the checkbox or radio button.
605   * 
606   * @returns {boolean} - True if the checkbox or radio button is checked, false otherwise.
607   */
608  isChecked() {
609    return this.checkbox.checked;
610  }
611
612
613  /**
614   * Sets the indeterminate state of the checkbox.
615   * * @param {boolean} value - True to set the checkbox to indeterminate, false otherwise.
616   */
617  setIndeterminate(value) {
618    this.checkbox.indeterminate = Boolean(value);
619  }
620
621
622  /**
623   * Sets the checked state of the checkbox or radio button,
624   * and updates the "checked" attribute accordingly.
625   *
626   * @param {boolean} value - True to check the element, false to uncheck it.
627   */
628  set checked(value) {
629    const isChecked = Boolean(value);
630    this.checkbox.checked = isChecked;
631
632    if (isChecked) {
633      this.setAttribute('checked', '');
634    } else {
635      this.removeAttribute('checked');
636    }
637  }
638}
639
640// Only define the custom element if it hasn't been defined yet
641if (!customElements.get('checkbox-element')) {
642  customElements.define('checkbox-element', CheckboxElement);
643}

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.