// from: https://github.com/Mashiane/DaisyUI-MultiSelect-with-CheckBoxes // Daisy Multi-Select Component // Auto-inject styles if (!document.getElementById('daisy-multiselect-styles')) { const style = document.createElement('style'); style.id = 'daisy-multiselect-styles'; style.textContent = ` /* CRITICAL: Override DaisyUI menu-active globally for entire component */ daisy-multiselect .menu-active, daisy-multiselect li.menu-active, daisy-multiselect .menu li:active, daisy-multiselect .menu li:focus, daisy-multiselect .menu-vertical li:active, daisy-multiselect .menu-vertical li:focus { background-color: transparent !important; background: transparent !important; color: inherit !important; } /* Additional custom styles for the multi-select */ .multiselect-dropdown { max-height: 300px; overflow-y: auto; } /* Prevent text flicker during updates */ .multiselect-display { user-select: none; -webkit-user-select: none; backface-visibility: hidden; -webkit-font-smoothing: subpixel-antialiased; transform: translateZ(0); /* Force GPU acceleration */ contain: layout style; /* Isolate from external layout changes */ } /* Disabled state border */ .multiselect-trigger.input-disabled { border-color: rgba(0, 0, 0, 0.1); border-width: 1px; border-style: solid; } .multiselect-option { cursor: pointer; transition: background-color 0.15s ease-out; } /* Prevent text content from transitioning/fading during selection */ .multiselect-option * { transition: none !important; } /* Re-enable transition only for checkbox */ .multiselect-option input[type="checkbox"] { transition: none !important; } /* Completely override ALL DaisyUI menu active/focus states */ .multiselect-option:focus, .multiselect-option:focus-visible, .multiselect-option:focus-within, .multiselect-option:active, .multiselect-option.active, .multiselect-option.menu-active, .multiselect-option[aria-pressed="true"], li.multiselect-option:focus, li.multiselect-option:focus-visible, li.multiselect-option:focus-within, li.multiselect-option:active, .menu li.multiselect-option:focus, .menu li.multiselect-option:active, .menu-vertical li.multiselect-option:focus, .menu-vertical li.multiselect-option:active { background-color: transparent !important; background: transparent !important; color: inherit !important; outline: none !important; box-shadow: none !important; } /* Override DaisyUI menu internal opacity/color changes on child elements */ /* Exclude checkbox to preserve its checked color */ .multiselect-option:focus *:not(input[type="checkbox"]), .multiselect-option:focus-visible *:not(input[type="checkbox"]), .multiselect-option:active *:not(input[type="checkbox"]), .multiselect-option.active *:not(input[type="checkbox"]), .multiselect-option.menu-active *:not(input[type="checkbox"]), li.multiselect-option:focus *:not(input[type="checkbox"]), li.multiselect-option:active *:not(input[type="checkbox"]), .menu li.multiselect-option:focus *:not(input[type="checkbox"]), .menu li.multiselect-option:active *:not(input[type="checkbox"]), .menu-vertical li.multiselect-option:focus *:not(input[type="checkbox"]), .menu-vertical li.multiselect-option:active *:not(input[type="checkbox"]) { opacity: 1 !important; color: inherit !important; background: transparent !important; } .multiselect-option:hover:not(.disabled) { background-color: var(--color-base-200); } .multiselect-option.selected.highlight { background-color: var(--color-primary); color: var(--color-primary-content); } /* Color variants for selected items with highlight */ .multiselect-option.selected.highlight.color-secondary { background-color: var(--color-secondary); color: var(--color-secondary-content); } .multiselect-option.selected.highlight.color-accent { background-color: var(--color-accent); color: var(--color-accent-content); } .multiselect-option.selected.highlight.color-success { background-color: var(--color-success); color: var(--color-success-content); } .multiselect-option.selected.highlight.color-warning { background-color: var(--color-warning); color: var(--color-warning-content); } .multiselect-option.selected.highlight.color-error { background-color: var(--color-error); color: var(--color-error-content); } .multiselect-option.selected.highlight.color-info { background-color: var(--color-info); color: var(--color-info-content); } .multiselect-option.selected.highlight.color-neutral { background-color: var(--color-neutral); color: var(--color-neutral-content); } /* Keyboard navigation focus state */ .multiselect-option.focused > div { outline: 2px solid oklch(var(--p)); outline-offset: -2px; } /* Clear button inside select */ .clear-btn-inner { background: transparent; border: none; padding: 0.25rem; cursor: pointer; border-radius: 0.25rem; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } .clear-btn-inner:hover { background-color: rgba(0, 0, 0, 0.1); } /* Ensure button maintains flex layout */ .multiselect-trigger { display: flex !important; } /* Show clear button on hover or when there are selections */ .multiselect-container:hover .clear-btn-inner.has-selections, .clear-btn-inner.has-selections { opacity: 0.6 !important; } .multiselect-container:hover .clear-btn-inner.has-selections:hover { opacity: 1 !important; } /* Performance optimizations */ .multiselect-container { contain: layout style; } .multiselect-dropdown { contain: layout style; will-change: transform; } .chips-container { contain: layout style; } /* Hardware acceleration for smooth interactions */ .multiselect-option, .badge, .clear-btn-inner { transform: translateZ(0); backface-visibility: hidden; -webkit-font-smoothing: subpixel-antialiased; } /* Optimize checkbox transitions */ .multiselect-option input[type="checkbox"] { transition: none; /* Remove transition for instant feedback */ } /* Smooth chip animations */ .badge { transition: opacity 0.15s ease-out; } `; document.head.appendChild(style); } /** * @typedef {Object} SelectOption * @property {string} value - The option value * @property {string} text - The display text * @property {boolean} selected - Whether the option is selected * @property {boolean} disabled - Whether the option is disabled * @property {HTMLElement} optionEl - The option DOM element * @property {HTMLOptionElement} nativeOption - The native select option element */ /** * @typedef {Object} SelectChangeEventDetail * @property {string[]} values - Array of selected values * @property {string} valueString - Semicolon-delimited string of values */ /** * DaisyUI MultiSelect Custom Element * A feature-rich multi-select dropdown component with DaisyUI styling * * @class DaisyMultiSelect * @extends HTMLElement * * @fires change - Dispatched when selection changes (bubbles to native select) * * @example * * * * */ class DaisyMultiSelect extends HTMLElement { // Class constants static VALID_SIZES = ['xs', 'sm', 'md', 'lg', 'xl']; static VALID_COLORS = ['primary', 'secondary', 'accent', 'success', 'warning', 'error', 'info', 'neutral']; static DEFAULT_SIZE = 'md'; static DEFAULT_COLOR = 'primary'; static DEFAULT_VIRTUAL_SCROLL_THRESHOLD = 50; /** * Debounce utility - delays function execution until after wait time has elapsed * @param {Function} fn - The function to debounce * @param {number} wait - The delay in milliseconds * @returns {Function} The debounced function * @static */ static debounce(fn, wait) { let timeoutId; return function debounced(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn.apply(this, args), wait); }; } /** * Throttle utility - ensures function is called at most once per wait period * @param {Function} fn - The function to throttle * @param {number} wait - The minimum time between calls in milliseconds * @returns {Function} The throttled function * @static */ static throttle(fn, wait) { let lastCall = 0; return function throttled(...args) { const now = Date.now(); if (now - lastCall >= wait) { lastCall = now; fn.apply(this, args); } }; } constructor() { super(); this._keysSelected = {}; this._valueDict = {}; this._isOpen = false; this._optionIdCounter = 0; // For generating unique keys this._updateDisplayPending = false; // For microtask batching (reactive system) this._abortController = new AbortController(); // For event listener cleanup this._selectedCount = 0; // Cache selected count for performance this._cachedSelectedItems = []; // Cache selected items array to avoid rebuilding this._selectedItemsDirty = true; // Track if cache needs refresh // Debug mode this._debug = false; // Enable debug logging with 'debug' or 'verbose' attribute // Lifecycle hooks this._hooks = { onSetupStart: [], onSetupComplete: [], onRenderStart: [], onRenderComplete: [], onOptionAdded: [], onOptionRemoved: [], onSelectionChange: [] }; // Configuration options (can be set via configure() method) this._customRenderer = null; // Custom renderer function for dropdown options // Field mappings for addOptions bulk operation this._valueField = 'value'; // Field name for option value this._textField = 'text'; // Field name for option text this._selectedField = 'selected'; // Field name for selected state this._disabledField = 'disabled'; // Field name for disabled state this._extraDataFields = []; // Array of field names to include as extra data } static get observedAttributes() { // Observe class/style for user-applied styling and checked-color for checkbox colors return ['class', 'style', 'checked-color']; } /** * Centralized error handler * @private * @param {Error} error - The error object * @param {string} context - Where the error occurred (method name) */ _handleError(error, context) { console.error(`DaisyMultiSelect error in ${context}:`, error); // Dispatch custom error event for developers to listen to this.dispatchEvent(new CustomEvent('error', { detail: { error, context, message: error.message }, bubbles: true, composed: true })); // Show user-friendly error message if component is visible if (this._triggerBtn && context !== 'connectedCallback') { this._showErrorState(`An error occurred: ${error.message}`); } } /** * Show error state in the UI * @private * @param {string} message - Error message to display */ _showErrorState(message) { if (!this._display) return; const errorHtml = ` ${message} `; this._display.innerHTML = errorHtml; } /** * Extract data-* attributes from an element * @private * @param {HTMLElement} element - Element to extract data attributes from * @returns {Object} Object containing all data attributes (keys without 'data-' prefix) * @example * //