|
7 | 7 | // Import Choices.js dynamically to avoid SSR issues
|
8 | 8 | let Choices: any;
|
9 | 9 |
|
| 10 | + // Extend SelectItem to allow additional properties for group key functionality |
| 11 | + type ExtendedSelectItem = SelectItem & { [key: string]: any }; |
| 12 | + type ExtendedSelectGroup = SelectGroup & { choices: ExtendedSelectItem[] }; |
| 13 | +
|
10 | 14 | let {
|
11 | 15 | // Core attributes - pass through to Select component
|
12 | 16 | id,
|
|
59 | 63 | minLength = 0,
|
60 | 64 | tTooShort = (n: number) => `Enter ${n} or more characters for suggestions`,
|
61 | 65 |
|
| 66 | + // Group key for displaying additional context in options |
| 67 | + groupKey = undefined, |
| 68 | +
|
62 | 69 | // Custom styling props
|
63 | 70 | choicesItemBackgroundColor = "#f3f2f1",
|
64 | 71 | choicesItemBorderColor = "#b1b4b6",
|
|
68 | 75 | }: {
|
69 | 76 | id: string;
|
70 | 77 | name: string;
|
71 |
| - items?: SelectItem[]; |
72 |
| - groups?: SelectGroup[]; |
| 78 | + items?: ExtendedSelectItem[]; |
| 79 | + groups?: ExtendedSelectGroup[]; |
73 | 80 | value?: (string | number)[] | string | number | undefined;
|
74 | 81 | multiple?: boolean;
|
75 | 82 | label: string;
|
|
94 | 101 | sourceSelector?: (query: string, options: any[]) => "api" | "options";
|
95 | 102 | minLength?: number;
|
96 | 103 | tTooShort?: (n: number) => string;
|
| 104 | + groupKey?: string; |
97 | 105 | choicesItemBackgroundColor?: string;
|
98 | 106 | choicesItemBorderColor?: string;
|
99 | 107 | choicesItemTextColor?: string;
|
|
117 | 125 | let lastQuery = "";
|
118 | 126 | const baseNoChoicesText = "No choices to choose from";
|
119 | 127 |
|
| 128 | + // Helper function for getting group text |
| 129 | + function getGroupText(item: any): string | undefined { |
| 130 | + if (!groupKey || !item || typeof item !== "object") return undefined; |
| 131 | + return item[groupKey] ? String(item[groupKey]) : undefined; |
| 132 | + } |
| 133 | +
|
| 134 | + // HTML escaping function (simple version) |
| 135 | + function escapeHtml(text: string): string { |
| 136 | + if (typeof document === "undefined") return text; // SSR safety |
| 137 | + const div = document.createElement("div"); |
| 138 | + div.textContent = text; |
| 139 | + return div.innerHTML; |
| 140 | + } |
| 141 | +
|
120 | 142 | // Computed values for component configuration
|
121 | 143 | let computedPlaceholderText = $derived(
|
122 | 144 | placeholderText || (multiple ? "Select all that apply" : "Select one"),
|
|
146 | 168 | const flattened: ChoiceItem[] = [];
|
147 | 169 | // Include enhancedItems() first (single-select placeholder support)
|
148 | 170 | for (const it of enhancedItems) {
|
| 171 | + const groupText = getGroupText(it); |
| 172 | + const safeLabel = escapeHtml(String(it.text)); |
| 173 | + const safeGroup = groupText ? escapeHtml(groupText) : ""; |
| 174 | +
|
149 | 175 | flattened.push({
|
150 | 176 | value: it.value,
|
151 |
| - label: String(it.text), |
| 177 | + label: safeGroup |
| 178 | + ? `<span class="choices__item-label"> |
| 179 | + <span class="choices__item-main">${safeLabel}</span> |
| 180 | + <span class="gem-c-select-with-search__suggestion-group">${safeGroup}</span> |
| 181 | + </span>` |
| 182 | + : safeLabel, |
152 | 183 | disabled: it.disabled,
|
153 | 184 | });
|
154 | 185 | }
|
155 | 186 | // Then any explicit groups
|
156 | 187 | for (const g of groups) {
|
157 | 188 | for (const choice of g.choices) {
|
| 189 | + const groupText = getGroupText(choice); |
| 190 | + const safeLabel = escapeHtml(String(choice.text)); |
| 191 | + const safeGroup = groupText ? escapeHtml(groupText) : ""; |
| 192 | +
|
158 | 193 | flattened.push({
|
159 | 194 | value: choice.value,
|
160 |
| - label: String(choice.text), |
| 195 | + label: safeGroup |
| 196 | + ? `<span class="choices__item-label"> |
| 197 | + <span class="choices__item-main">${safeLabel}</span> |
| 198 | + <span class="gem-c-select-with-search__suggestion-group">${safeGroup}</span> |
| 199 | + </span>` |
| 200 | + : safeLabel, |
161 | 201 | disabled: g.disabled || choice.disabled,
|
162 | 202 | });
|
163 | 203 | }
|
|
231 | 271 | | any[]
|
232 | 272 | | undefined;
|
233 | 273 | if (!Array.isArray(data)) return [];
|
| 274 | +
|
234 | 275 | const mapped: ChoiceItem[] = data.map((entry) => {
|
235 | 276 | const label = toLabel(entry);
|
236 |
| - return { value: toValue(entry, label), label }; |
| 277 | + const groupText = getGroupText(entry); |
| 278 | + const safeLabel = escapeHtml(label); |
| 279 | + const safeGroup = groupText ? escapeHtml(groupText) : ""; |
| 280 | +
|
| 281 | + return { |
| 282 | + value: toValue(entry, label), |
| 283 | + label: safeGroup |
| 284 | + ? `<span class="choices__item-label"> |
| 285 | + <span class="choices__item-main">${safeLabel}</span> |
| 286 | + <span class="gem-c-select-with-search__suggestion-group">${safeGroup}</span> |
| 287 | + </span>` |
| 288 | + : safeLabel, |
| 289 | + }; |
237 | 290 | });
|
| 291 | +
|
238 | 292 | return mapped;
|
239 | 293 | }
|
240 | 294 |
|
|
443 | 497 | });
|
444 | 498 |
|
445 | 499 | // Check if placeholder option already exists
|
446 |
| - const existingPlaceholder = |
447 |
| - selectElement.querySelector('option[value=""]'); |
| 500 | + const existingPlaceholder = selectElement.querySelector( |
| 501 | + 'option[value=""]', |
| 502 | + ) as HTMLOptionElement | null; |
448 | 503 | console.log("🔍 Existing placeholder check:", {
|
449 | 504 | found: !!existingPlaceholder,
|
450 | 505 | placeholder: existingPlaceholder
|
|
469 | 524 | console.log("✅ Added placeholder option to DOM");
|
470 | 525 | }
|
471 | 526 |
|
| 527 | + // If groupKey is provided, update all existing options to include group text |
| 528 | + if (groupKey && selectElement) { |
| 529 | + console.log("🔧 Updating DOM options with group text"); |
| 530 | + // Update items options |
| 531 | + items.forEach((item, index) => { |
| 532 | + const optionIndex = multiple ? index : index + 1; // +1 for placeholder |
| 533 | + if (selectElement && selectElement.options[optionIndex]) { |
| 534 | + const groupText = getGroupText(item); |
| 535 | + const safeLabel = escapeHtml(String(item.text)); |
| 536 | + const safeGroup = groupText ? escapeHtml(groupText) : ""; |
| 537 | + const option = selectElement.options[optionIndex]; |
| 538 | +
|
| 539 | + if (safeGroup) { |
| 540 | + option.innerHTML = `<span class="choices__item-label"> |
| 541 | + <span class="choices__item-main">${safeLabel}</span> |
| 542 | + <span class="gem-c-select-with-search__suggestion-group">${safeGroup}</span> |
| 543 | + </span>`; |
| 544 | + console.log( |
| 545 | + "✅ Updated item option with group text:", |
| 546 | + option.innerHTML, |
| 547 | + ); |
| 548 | + } |
| 549 | + } |
| 550 | + }); |
| 551 | +
|
| 552 | + // Update grouped options |
| 553 | + let optionIndex = multiple ? items.length : items.length + 1; // +1 for placeholder |
| 554 | + groups.forEach((group) => { |
| 555 | + group.choices.forEach((choice) => { |
| 556 | + if (selectElement && selectElement.options[optionIndex]) { |
| 557 | + const groupText = getGroupText(choice); |
| 558 | + const safeLabel = escapeHtml(String(choice.text)); |
| 559 | + const safeGroup = groupText ? escapeHtml(groupText) : ""; |
| 560 | + const option = selectElement.options[optionIndex]; |
| 561 | +
|
| 562 | + if (safeGroup) { |
| 563 | + option.innerHTML = `<span class="choices__item-label"> |
| 564 | + <span class="choices__item-main">${safeLabel}</span> |
| 565 | + <span class="gem-c-select-with-search__suggestion-group">${safeGroup}</span> |
| 566 | + </span>`; |
| 567 | + console.log( |
| 568 | + "✅ Updated grouped option with group text:", |
| 569 | + option.innerHTML, |
| 570 | + ); |
| 571 | + } |
| 572 | + } |
| 573 | + optionIndex++; |
| 574 | + }); |
| 575 | + }); |
| 576 | + } |
| 577 | +
|
472 | 578 | // Log the DOM structure after ensuring placeholder exists
|
473 | 579 | console.log("📋 DOM structure AFTER placeholder check:", {
|
474 | 580 | totalOptions: selectElement.options.length,
|
475 | 581 | options: Array.from(selectElement.options).map((opt, idx) => ({
|
476 | 582 | index: idx,
|
477 | 583 | value: (opt as HTMLOptionElement).value,
|
478 | 584 | text: (opt as HTMLOptionElement).text,
|
| 585 | + innerHTML: (opt as HTMLOptionElement).innerHTML, |
479 | 586 | selected: (opt as HTMLOptionElement).selected,
|
480 | 587 | disabled: (opt as HTMLOptionElement).disabled,
|
481 | 588 | })),
|
|
537 | 644 | duplicateItemsAllowed: false,
|
538 | 645 | callbackOnInit: function () {
|
539 | 646 | console.log("🎉 Choices.js initialized successfully");
|
| 647 | +
|
| 648 | + // Apply group text to initial choices if groupKey is provided |
| 649 | + if (groupKey && this.choices && this.choices.length > 0) { |
| 650 | + console.log("🔧 Applying group text to initial choices"); |
| 651 | + this.choices.forEach((choice: any) => { |
| 652 | + if ( |
| 653 | + choice && |
| 654 | + choice.label && |
| 655 | + !choice.label.includes( |
| 656 | + '<span class="gem-c-select-with-search__suggestion-group">', |
| 657 | + ) |
| 658 | + ) { |
| 659 | + // Find the original item to get group text |
| 660 | + const originalItem = staticChoices.find( |
| 661 | + (item) => String(item.value) === String(choice.value), |
| 662 | + ); |
| 663 | + if (originalItem && originalItem.label !== choice.label) { |
| 664 | + choice.label = originalItem.label; |
| 665 | + console.log( |
| 666 | + "✅ Updated choice label with group text:", |
| 667 | + choice.label, |
| 668 | + ); |
| 669 | + } |
| 670 | + } |
| 671 | + }); |
| 672 | + } |
| 673 | +
|
540 | 674 | // For multiple select, move input field to top of feedback area
|
541 | 675 | if (this.dropdown.type === "select-multiple") {
|
542 | 676 | const inner = this.containerInner.element;
|
|
560 | 694 | // Store reference on the element for external access
|
561 | 695 | (selectElement as any).choices = choicesInstance;
|
562 | 696 |
|
| 697 | + // Ensure initial choices have group text applied if groupKey is provided |
| 698 | + if (groupKey && choicesInstance && staticChoices.length > 0) { |
| 699 | + console.log("🔧 Ensuring initial choices have group text applied"); |
| 700 | + // Force refresh of choices with group text |
| 701 | + setTimeout(() => { |
| 702 | + if (choicesInstance) { |
| 703 | + choicesInstance.clearChoices(); |
| 704 | + choicesInstance.setChoices( |
| 705 | + staticChoices.map((c) => ({ |
| 706 | + value: String(c.value), |
| 707 | + label: c.label, |
| 708 | + disabled: c.disabled, |
| 709 | + })), |
| 710 | + "value", |
| 711 | + "label", |
| 712 | + true, |
| 713 | + ); |
| 714 | + console.log("✅ Initial choices refreshed with group text"); |
| 715 | + } |
| 716 | + }, 0); |
| 717 | + } |
| 718 | +
|
563 | 719 | // Log the DOM structure after Choices.js initialization
|
564 | 720 | console.log("🔍 DOM structure AFTER Choices.js initialization:", {
|
565 | 721 | totalOptions: selectElement.options.length,
|
|
961 | 1117 | <div
|
962 | 1118 | class="gem-c-select-with-search"
|
963 | 1119 | style={`--cross-icon-url: url("${crossIconUrl}"); --choices-item-bg-color: ${choicesItemBackgroundColor}; --choices-item-border-color: ${choicesItemBorderColor}; --choices-item-text-color: ${choicesItemTextColor}; --choices-item-divider-padding: ${choicesItemDividerPadding};`}
|
| 1120 | + data-group-key={groupKey} |
964 | 1121 | >
|
965 | 1122 | {#snippet rightIcon()}
|
966 | 1123 | <button
|
|
1841 | 1998 | ) {
|
1842 | 1999 | padding: 4px 8px;
|
1843 | 2000 | }
|
| 2001 | +
|
| 2002 | + /* Group text styling similar to SearchAutocomplete */ |
| 2003 | + :global( |
| 2004 | + .gem-c-select-with-search |
| 2005 | + .choices__item |
| 2006 | + .gem-c-select-with-search__suggestion-group |
| 2007 | + ) { |
| 2008 | + opacity: 0.8; |
| 2009 | + font-size: smaller; |
| 2010 | + font-weight: normal; |
| 2011 | + } |
| 2012 | +
|
| 2013 | + /* Flex container for label + group text alignment */ |
| 2014 | + :global(.gem-c-select-with-search .choices__item-label) { |
| 2015 | + display: inline-flex; |
| 2016 | + align-items: baseline; /* aligns text baselines for consistent vertical alignment */ |
| 2017 | + gap: 5px; /* spacing between label and group text */ |
| 2018 | + } |
| 2019 | +
|
| 2020 | + /* Main text styling */ |
| 2021 | + /* :global(.gem-c-select-with-search .choices__item-main) { |
| 2022 | + font-weight: bold; |
| 2023 | + } */ |
| 2024 | +
|
| 2025 | + /* Override the bold weight for the group text specifically */ |
| 2026 | + :global( |
| 2027 | + .gem-c-select-with-search |
| 2028 | + .choices__item |
| 2029 | + .gem-c-select-with-search__suggestion-group |
| 2030 | + ) { |
| 2031 | + font-weight: normal; |
| 2032 | + } |
1844 | 2033 | </style>
|
0 commit comments