diff --git a/.gitignore b/.gitignore index 5feebbc..3932aac 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ tailwindcss .env .env* default.profraw -./rooms.csv +/rooms.csv diff --git a/Public/css/output.css b/Public/css/output.css index 2c733bb..57b317c 100644 --- a/Public/css/output.css +++ b/Public/css/output.css @@ -7,9 +7,7 @@ 'Noto Color Emoji'; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; - --color-amber-300: oklch(87.9% 0.169 91.605); --color-green-400: oklch(79.2% 0.209 151.711); - --color-sky-300: oklch(82.8% 0.111 230.318); --color-sky-600: oklch(58.8% 0.158 241.966); --color-violet-600: oklch(54.1% 0.281 293.009); --color-gray-200: oklch(92.8% 0.006 264.531); @@ -17,12 +15,12 @@ --color-black: #000; --color-white: #fff; --spacing: 0.25rem; - --container-md: 28rem; - --container-lg: 32rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75 / 1.125); --text-xl: 1.25rem; @@ -41,7 +39,6 @@ --radius-sm: 0.25rem; --radius-md: 0.375rem; --radius-lg: 0.5rem; - --radius-xl: 0.75rem; --radius-3xl: 1.5rem; --ease-out: cubic-bezier(0, 0, 0.2, 1); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); @@ -1701,6 +1698,9 @@ opacity: 40%; } } + .pointer-events-none { + pointer-events: none; + } .react-day-picker { @layer daisyui.l1.l2.l3 { user-select: none; @@ -4232,18 +4232,12 @@ --toast-y: 0; } } - .top-0 { - top: calc(var(--spacing) * 0); - } .top-2 { top: calc(var(--spacing) * 2); } .top-10 { top: calc(var(--spacing) * 10); } - .top-20 { - top: calc(var(--spacing) * 20); - } .right-2 { right: calc(var(--spacing) * 2); } @@ -4307,21 +4301,12 @@ .bottom-0 { bottom: calc(var(--spacing) * 0); } - .-left-10 { - left: calc(var(--spacing) * -10); - } .-left-15 { left: calc(var(--spacing) * -15); } - .-left-20 { - left: calc(var(--spacing) * -20); - } .left-0 { left: calc(var(--spacing) * 0); } - .left-10 { - left: calc(var(--spacing) * 10); - } .join { display: inline-flex; align-items: stretch; @@ -4711,6 +4696,9 @@ .z-1 { z-index: 1; } + .z-50 { + z-index: 50; + } .tab-content { @layer daisyui.l1.l2.l3 { order: var(--tabcontent-order); @@ -5314,9 +5302,6 @@ } } } - .mx-10 { - margin-inline: calc(var(--spacing) * 10); - } .mx-20 { margin-inline: calc(var(--spacing) * 20); } @@ -5409,9 +5394,6 @@ } } } - .my-1 { - margin-block: calc(var(--spacing) * 1); - } .my-1\.5 { margin-block: calc(var(--spacing) * 1.5); } @@ -5681,9 +5663,6 @@ .-mt-2 { margin-top: calc(var(--spacing) * -2); } - .mt-1 { - margin-top: calc(var(--spacing) * 1); - } .mt-2 { margin-top: calc(var(--spacing) * 2); } @@ -5699,12 +5678,6 @@ .mt-10 { margin-top: calc(var(--spacing) * 10); } - .mt-20 { - margin-top: calc(var(--spacing) * 20); - } - .mt-60 { - margin-top: calc(var(--spacing) * 60); - } .breadcrumbs { @layer daisyui.l1.l2.l3 { max-width: 100%; @@ -5761,6 +5734,9 @@ } } } + .mr-2 { + margin-right: calc(var(--spacing) * 2); + } .fieldset-legend { @layer daisyui.l1.l2.l3 { margin-bottom: calc(0.25rem * -1); @@ -5781,6 +5757,9 @@ font-weight: 600; } } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } .mb-4 { margin-bottom: calc(var(--spacing) * 4); } @@ -6505,6 +6484,33 @@ } } } + .\!h-auto { + height: auto !important; + } + .h-3 { + height: calc(var(--spacing) * 3); + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-6 { + height: calc(var(--spacing) * 6); + } + .h-8 { + height: calc(var(--spacing) * 8); + } + .h-10 { + height: calc(var(--spacing) * 10); + } + .h-12 { + height: calc(var(--spacing) * 12); + } + .h-14 { + height: calc(var(--spacing) * 14); + } .h-\[1em\] { height: 1em; } @@ -6514,6 +6520,21 @@ .h-full { height: 100%; } + .min-h-6 { + min-height: calc(var(--spacing) * 6); + } + .min-h-8 { + min-height: calc(var(--spacing) * 8); + } + .min-h-10 { + min-height: calc(var(--spacing) * 10); + } + .min-h-12 { + min-height: calc(var(--spacing) * 12); + } + .min-h-14 { + min-height: calc(var(--spacing) * 14); + } .min-h-\[400px\] { min-height: 400px; } @@ -6648,8 +6669,17 @@ width: calc(var(--size-selector, 0.25rem) * 4); } } - .w-\[200px\] { - width: 200px; + .w-3 { + width: calc(var(--spacing) * 3); + } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-5 { + width: calc(var(--spacing) * 5); + } + .w-6 { + width: calc(var(--spacing) * 6); } .w-\[250px\] { width: 250px; @@ -6663,8 +6693,11 @@ .w-full { width: 100%; } - .min-w-\[80\%\] { - min-width: 80%; + .w-px { + width: 1px; + } + .min-w-0 { + min-width: calc(var(--spacing) * 0); } .min-w-\[200px\] { min-width: 200px; @@ -6684,6 +6717,12 @@ .flex-shrink { flex-shrink: 1; } + .flex-shrink-0 { + flex-shrink: 0; + } + .shrink-0 { + flex-shrink: 0; + } .flex-grow { flex-grow: 1; } @@ -6709,9 +6748,6 @@ .-rotate-45 { rotate: calc(45deg * -1); } - .rotate-45 { - rotate: 45deg; - } .rotate-180 { rotate: 180deg; } @@ -6770,6 +6806,12 @@ } } } + .cursor-not-allowed { + cursor: not-allowed; + } + .cursor-pointer { + cursor: pointer; + } .resize { resize: both; } @@ -6889,12 +6931,12 @@ .flex-col { flex-direction: column; } + .flex-row { + flex-direction: row; + } .flex-wrap { flex-wrap: wrap; } - .items-baseline { - align-items: baseline; - } .items-center { align-items: center; } @@ -6916,12 +6958,18 @@ .justify-start { justify-content: flex-start; } + .gap-0 { + gap: calc(var(--spacing) * 0); + } .gap-1 { gap: calc(var(--spacing) * 1); } .gap-2 { gap: calc(var(--spacing) * 2); } + .gap-3 { + gap: calc(var(--spacing) * 3); + } .gap-4 { gap: calc(var(--spacing) * 4); } @@ -6960,6 +7008,9 @@ margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse))); } } + .gap-x-2 { + column-gap: calc(var(--spacing) * 2); + } .space-x-2 { :where(& > :not(:last-child)) { --tw-space-x-reverse: 0; @@ -7072,6 +7123,9 @@ } } } + .rounded { + border-radius: 0.25rem; + } .rounded-3xl { border-radius: var(--radius-3xl); } @@ -7087,6 +7141,9 @@ .rounded-field { border-radius: var(--radius-field); } + .rounded-full { + border-radius: calc(infinity * 1px); + } .rounded-lg { border-radius: var(--radius-lg); } @@ -7290,10 +7347,6 @@ border-bottom-style: var(--tw-border-style); border-bottom-width: 6px; } - .border-b-8 { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 8px; - } .badge-dash { @layer daisyui.l1.l2 { color: var(--badge-color); @@ -7406,6 +7459,9 @@ .border-accent { border-color: var(--color-accent); } + .border-base-300 { + border-color: var(--color-base-300); + } .border-error { border-color: var(--color-error); } @@ -7415,11 +7471,8 @@ .border-primary { border-color: var(--color-primary); } - .border-secondary { - border-color: var(--color-secondary); - } - .border-sky-600 { - border-color: var(--color-sky-600); + .border-success { + border-color: var(--color-success); } .menu-active { :where(:not(ul, details, .menu-title, .btn))& { @@ -7578,18 +7631,15 @@ .bg-base-300 { background-color: var(--color-base-300); } + .bg-current { + background-color: currentcolor; + } .bg-error { background-color: var(--color-error); } - .bg-primary { - background-color: var(--color-primary); - } .bg-secondary { background-color: var(--color-secondary); } - .bg-violet-600 { - background-color: var(--color-violet-600); - } .divider-accent { @layer daisyui.l1.l2 { &:before, &:after { @@ -7799,18 +7849,12 @@ } } } - .mask-contain { - mask-size: contain; - } - .mask-clip-border { - mask-clip: border-box; - } - .mask-clip-content { - mask-clip: content-box; - } .mask-repeat { mask-repeat: repeat; } + .stroke-current { + stroke: currentcolor; + } .checkbox-lg { @layer daisyui.l1.l2 { padding: 0.3125rem; @@ -7881,9 +7925,24 @@ } } } + .\!p-1\.5 { + padding: calc(var(--spacing) * 1.5) !important; + } + .p-1 { + padding: calc(var(--spacing) * 1); + } + .p-1\.5 { + padding: calc(var(--spacing) * 1.5); + } .p-2 { padding: calc(var(--spacing) * 2); } + .p-2\.5 { + padding: calc(var(--spacing) * 2.5); + } + .p-3 { + padding: calc(var(--spacing) * 3); + } .p-4 { padding: calc(var(--spacing) * 4); } @@ -8007,12 +8066,27 @@ } } } + .\!px-0 { + padding-inline: calc(var(--spacing) * 0) !important; + } + .\!px-3 { + padding-inline: calc(var(--spacing) * 3) !important; + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } .px-4 { padding-inline: calc(var(--spacing) * 4); } .px-6 { padding-inline: calc(var(--spacing) * 6); } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } .py-2 { padding-block: calc(var(--spacing) * 2); } @@ -8045,6 +8119,9 @@ .text-center { text-align: center; } + .text-left { + text-align: left; + } .file-input-lg { @layer daisyui.l1.l2 { --size: calc(var(--size-field, 0.25rem) * 12); @@ -8093,10 +8170,6 @@ font-size: var(--text-3xl); line-height: var(--tw-leading, var(--text-3xl--line-height)); } - .text-4xl { - font-size: var(--text-4xl); - line-height: var(--tw-leading, var(--text-4xl--line-height)); - } .text-5xl { font-size: var(--text-5xl); line-height: var(--tw-leading, var(--text-5xl--line-height)); @@ -8105,6 +8178,10 @@ font-size: var(--text-8xl); line-height: var(--tw-leading, var(--text-8xl--line-height)); } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } .text-lg { font-size: var(--text-lg); line-height: var(--tw-leading, var(--text-lg--line-height)); @@ -8675,9 +8752,6 @@ color: var(--color-warning); } } - .text-accent { - color: var(--color-accent); - } .text-base-content { color: var(--color-base-content); } @@ -8696,9 +8770,6 @@ .text-primary { color: var(--color-primary); } - .text-secondary { - color: var(--color-secondary); - } .text-success { color: var(--color-success); } @@ -8763,9 +8834,18 @@ } } } + .opacity-0 { + opacity: 0%; + } + .opacity-30 { + opacity: 30%; + } .opacity-50 { opacity: 50%; } + .opacity-60 { + opacity: 60%; + } .shadow-2xl { --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -8818,6 +8898,16 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } + .transition-opacity { + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-transform { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } .ease-in-out { --tw-ease: var(--ease-in-out); transition-timing-function: var(--ease-in-out); @@ -9573,6 +9663,44 @@ } } } + .hover\:opacity-75 { + &:hover { + @media (hover: hover) { + opacity: 75%; + } + } + } + .hover\:opacity-100 { + &:hover { + @media (hover: hover) { + opacity: 100%; + } + } + } + .focus\:\!bg-transparent { + &:focus { + background-color: transparent !important; + } + } + .focus\:outline-none { + &:focus { + --tw-outline-style: none; + outline-style: none; + } + } + .active\:scale-100 { + &:active { + --tw-scale-x: 100%; + --tw-scale-y: 100%; + --tw-scale-z: 100%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + } + .active\:\!bg-transparent { + &:active { + background-color: transparent !important; + } + } .data-active\:bg-neutral { &[data-active] { background-color: var(--color-neutral); @@ -9661,11 +9789,6 @@ grid-template-columns: repeat(4, minmax(0, 1fr)); } } - .dark\:bg-base-300 { - @media (prefers-color-scheme: dark) { - background-color: var(--color-base-300); - } - } .is-drawer-close\:mx-auto { &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { margin-inline: auto; @@ -9705,11 +9828,6 @@ overflow: visible; } } - .is-drawer-close\:text-error { - &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { - color: var(--color-error); - } - } .is-drawer-close\:text-green-400 { &:where(.drawer-toggle:not(:checked) ~ .drawer-side, .drawer-toggle:not(:checked) ~ .drawer-side *) { color: var(--color-green-400); @@ -11297,6 +11415,21 @@ syntax: "*"; inherits: false; } +@property --tw-scale-x { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-y { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-z { + syntax: "*"; + inherits: false; + initial-value: 1; +} @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { @@ -11352,6 +11485,9 @@ --tw-backdrop-saturate: initial; --tw-backdrop-sepia: initial; --tw-ease: initial; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-scale-z: 1; } } } diff --git a/Public/js/daisy-multiselect.js b/Public/js/daisy-multiselect.js new file mode 100644 index 0000000..4ec9e92 --- /dev/null +++ b/Public/js/daisy-multiselect.js @@ -0,0 +1,3090 @@ +// 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 + * //