@keenmate/web-multiselect 1.0.0-rc11 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +554 -89
- package/dist/multiselect.js +1404 -1174
- package/dist/multiselect.umd.js +35 -40
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/scss/{_pills-display.scss → _badges-display.scss} +58 -57
- package/src/scss/_base.scss +2 -2
- package/src/scss/_css-variables.scss +57 -54
- package/src/scss/_input-dropdown.scss +23 -11
- package/src/scss/_modifiers.scss +3 -3
- package/src/scss/_options.scss +14 -1
- package/src/scss/_rtl.scss +14 -14
- package/src/scss/_tooltips-popover.scss +28 -16
- package/src/scss/_variables.scss +58 -56
- package/src/scss/main.scss +4 -4
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ A lightweight, accessible multiselect web component with typeahead search, RTL l
|
|
|
12
12
|
- 🔍 **Flexible Search Modes** - Filter (hide non-matches) or navigate (jump to matches, keep all visible)
|
|
13
13
|
- ⌨️ **Keyboard Navigation** - Full keyboard support (arrows, Enter, Esc, Tab)
|
|
14
14
|
- 🎨 **Rich Content** - Icons, subtitles, and multiline text support
|
|
15
|
-
- 📊 **Multiple Display Modes** -
|
|
16
|
-
- 💬 **
|
|
15
|
+
- 📊 **Multiple Display Modes** - Badges, count, compact, partial, or none (minimal UI)
|
|
16
|
+
- 💬 **Badge Tooltips** - Customizable tooltips on selected items with placement control
|
|
17
17
|
- 🎯 **Single & Multi-Select** - Switch between single and multiple selection modes
|
|
18
18
|
- 🔄 **Async Data Loading** - On-demand data fetching support
|
|
19
19
|
- 📦 **Grouped Options** - Organize options into collapsible groups
|
|
@@ -111,27 +111,26 @@ multiselect.setSelected(['js', 'ts']);
|
|
|
111
111
|
| `search-placeholder` | `string` | `'Search...'` | Placeholder text for search input |
|
|
112
112
|
| `search-hint` | `string` | - | Hint text shown above input when focused |
|
|
113
113
|
| `allow-groups` | `boolean` | `true` | Enable option grouping |
|
|
114
|
-
| `allow-select-all` | `boolean` | `true` | Show "Select All" button |
|
|
115
|
-
| `allow-clear-all` | `boolean` | `true` | Show "Clear All" button |
|
|
116
114
|
| `show-checkboxes` | `boolean` | `true` | Show checkboxes next to options |
|
|
117
115
|
| `close-on-select` | `boolean` | `false` | Close dropdown after selecting |
|
|
118
116
|
| `dropdown-min-width` | `string` | - | Min width for dropdown (e.g., '20rem') |
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `show-
|
|
125
|
-
| `enable-
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
117
|
+
| `badges-display-mode` | `'pills' \| 'count' \| 'compact' \| 'partial' \| 'none'` | `'pills'` | How to display selected items. `compact`: first item + count. `none`: no display |
|
|
118
|
+
| `badges-threshold` | `number` | - | Auto-switch mode when exceeded (see badges-threshold-mode) |
|
|
119
|
+
| `badges-threshold-mode` | `'count' \| 'partial'` | `'count'` | Mode after threshold: 'count' shows badge, 'partial' shows limited badges + more badge |
|
|
120
|
+
| `badges-max-visible` | `number` | `3` | Max badges shown in partial mode |
|
|
121
|
+
| `badges-position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | Position of badges container |
|
|
122
|
+
| `show-counter` | `boolean` | `false` | Show [3] badge next to toggle icon |
|
|
123
|
+
| `enable-badge-tooltips` | `boolean` | `false` | Enable tooltips on selected badges |
|
|
124
|
+
| `badge-tooltip-placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Tooltip placement relative to badge |
|
|
125
|
+
| `badge-tooltip-delay` | `number` | `300` | Delay in ms before showing tooltip |
|
|
126
|
+
| `badge-tooltip-offset` | `number` | `8` | Distance in pixels between badge and tooltip |
|
|
129
127
|
| `max-height` | `string` | `'20rem'` | Maximum height of dropdown |
|
|
130
128
|
| `empty-message` | `string` | `'No results found'` | Message when no options found |
|
|
131
129
|
| `loading-message` | `string` | `'Loading...'` | Message while loading async data |
|
|
132
130
|
| `min-search-length` | `number` | `0` | Minimum search length for async |
|
|
133
131
|
| `keep-options-on-search` | `boolean` | `true` | Keep initial options visible when searchCallback is active (hybrid search) |
|
|
134
|
-
| `sticky-actions` | `boolean` | `true` | Keep
|
|
132
|
+
| `sticky-actions` | `boolean` | `true` | Keep action buttons fixed at top while scrolling |
|
|
133
|
+
| `actions-layout` | `'nowrap' \| 'wrap'` | `'nowrap'` | Layout mode for action buttons: 'nowrap' (single row) or 'wrap' (multi-row) |
|
|
135
134
|
| `lock-placement` | `boolean` | `true` | Lock dropdown placement after first open to prevent flipping |
|
|
136
135
|
| `enable-search` | `boolean` | `true` | Enable/disable search functionality |
|
|
137
136
|
| `search-input-mode` | `'normal' \| 'readonly' \| 'hidden'` | `'normal'` | Search input display mode |
|
|
@@ -191,19 +190,53 @@ multiselect.onChange = (selectedOptions) => {
|
|
|
191
190
|
console.log('Changed:', selectedOptions);
|
|
192
191
|
};
|
|
193
192
|
|
|
194
|
-
//
|
|
195
|
-
multiselect.
|
|
196
|
-
// Show shorter text in
|
|
193
|
+
// Badge display customization (show different text in badges vs dropdown)
|
|
194
|
+
multiselect.getBadgeDisplayCallback = (item) => {
|
|
195
|
+
// Show shorter text in badges (e.g., just name instead of "name (email)")
|
|
197
196
|
return item.name; // Dropdown might show "John Doe (john@example.com)"
|
|
198
197
|
};
|
|
199
198
|
|
|
200
|
-
//
|
|
201
|
-
multiselect.
|
|
199
|
+
// Badge tooltip customization
|
|
200
|
+
multiselect.getBadgeTooltipCallback = (item) => {
|
|
202
201
|
return `${item.label} - ${item.subtitle}`;
|
|
203
202
|
};
|
|
204
203
|
|
|
205
|
-
//
|
|
206
|
-
multiselect.
|
|
204
|
+
// Action buttons (Select All, Clear All, custom actions)
|
|
205
|
+
multiselect.actionButtons = [
|
|
206
|
+
{
|
|
207
|
+
action: 'select-all',
|
|
208
|
+
text: 'Select All',
|
|
209
|
+
tooltip: 'Select all items',
|
|
210
|
+
cssClass: 'my-custom-class',
|
|
211
|
+
isVisibleCallback: (multiselect) => multiselect.getSelected().length < 5 // Hide if 5+ selected
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
action: 'clear-all',
|
|
215
|
+
text: 'Clear All',
|
|
216
|
+
tooltip: 'Clear selection',
|
|
217
|
+
isVisible: true, // Static visibility
|
|
218
|
+
isDisabled: false // Static disabled state
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
action: 'custom',
|
|
222
|
+
text: 'Invert',
|
|
223
|
+
tooltip: 'Invert selection',
|
|
224
|
+
onClick: (multiselect) => {
|
|
225
|
+
// Custom action - invert selection
|
|
226
|
+
const allValues = multiselect.options.map(opt => opt.value);
|
|
227
|
+
const selectedValues = multiselect.getValue();
|
|
228
|
+
const inverted = allValues.filter(v => !selectedValues.includes(v));
|
|
229
|
+
multiselect.setSelected(inverted);
|
|
230
|
+
},
|
|
231
|
+
// Dynamic callbacks (take priority over static properties)
|
|
232
|
+
isDisabledCallback: (multiselect) => multiselect.getSelected().length === 0,
|
|
233
|
+
getTextCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'Invert' : 'Select Items First',
|
|
234
|
+
getClassCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'active' : 'inactive'
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
// Counter i18n/pluralization
|
|
239
|
+
multiselect.getCounterCallback = (count, moreCount) => {
|
|
207
240
|
if (moreCount !== undefined) {
|
|
208
241
|
return `+${moreCount} more`; // Partial mode badge
|
|
209
242
|
}
|
|
@@ -227,6 +260,24 @@ multiselect.getSubtitleCallback = (item) => `${item.price} - ${item.stock} in st
|
|
|
227
260
|
multiselect.getGroupCallback = (item) => item.category;
|
|
228
261
|
multiselect.getDisabledCallback = (item) => item.stock === 0;
|
|
229
262
|
|
|
263
|
+
// Custom rendering - Full HTML control
|
|
264
|
+
multiselect.renderOptionContentCallback = (item, context) => {
|
|
265
|
+
// Customize option content (HTML string or HTMLElement)
|
|
266
|
+
return `<strong>${item.name}</strong> <span class="badge">${item.status}</span>`;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
multiselect.renderBadgeContentCallback = (item, context) => {
|
|
270
|
+
// Customize badge content (HTML string or HTMLElement)
|
|
271
|
+
return context.isInPopover
|
|
272
|
+
? `${item.icon} ${item.name} - ${item.description}`
|
|
273
|
+
: `${item.icon} ${item.name}`;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
multiselect.renderSelectedContentCallback = (item) => {
|
|
277
|
+
// Customize selected item text in single-select mode (plain text only)
|
|
278
|
+
return item.firstName; // Show just first name when closed
|
|
279
|
+
};
|
|
280
|
+
|
|
230
281
|
// Form integration
|
|
231
282
|
multiselect.name = 'selected_items';
|
|
232
283
|
multiselect.valueFormat = 'json'; // 'json' | 'csv' | 'array'
|
|
@@ -560,86 +611,91 @@ Choose between two search behaviors:
|
|
|
560
611
|
Perfect for different use cases and space constraints:
|
|
561
612
|
|
|
562
613
|
```html
|
|
563
|
-
<!--
|
|
564
|
-
<web-multiselect
|
|
614
|
+
<!-- Badges mode (default) - Show all selections as removable badges -->
|
|
615
|
+
<web-multiselect badges-display-mode="pills"></web-multiselect>
|
|
565
616
|
|
|
566
617
|
<!-- Count mode - Show "X selected" text with clear button -->
|
|
567
|
-
<web-multiselect
|
|
618
|
+
<web-multiselect badges-display-mode="count" show-counter="true"></web-multiselect>
|
|
568
619
|
|
|
569
|
-
<!-- Compact mode - Show first item + count in a single removable
|
|
570
|
-
<web-multiselect
|
|
620
|
+
<!-- Compact mode - Show first item + count in a single removable badge -->
|
|
621
|
+
<web-multiselect badges-display-mode="compact"></web-multiselect>
|
|
571
622
|
<!-- Example output: [JavaScript (+2 more) | x] -->
|
|
572
623
|
|
|
573
|
-
<!-- None mode - No display in
|
|
574
|
-
<web-multiselect
|
|
624
|
+
<!-- None mode - No display in badges area (minimal UI) -->
|
|
625
|
+
<web-multiselect badges-display-mode="none" show-counter="true"></web-multiselect>
|
|
575
626
|
<!-- Only shows [X] badge next to toggle icon -->
|
|
576
627
|
|
|
577
|
-
<!-- Auto-switch from
|
|
628
|
+
<!-- Auto-switch from badges to count at threshold -->
|
|
578
629
|
<web-multiselect
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
show-
|
|
630
|
+
badges-threshold="3"
|
|
631
|
+
badges-threshold-mode="count"
|
|
632
|
+
show-counter="true">
|
|
582
633
|
</web-multiselect>
|
|
583
634
|
|
|
584
|
-
<!-- Partial mode - Show limited
|
|
635
|
+
<!-- Partial mode - Show limited badges + "+X more" badge -->
|
|
585
636
|
<web-multiselect
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
637
|
+
badges-threshold="5"
|
|
638
|
+
badges-threshold-mode="partial"
|
|
639
|
+
badges-max-visible="3">
|
|
589
640
|
</web-multiselect>
|
|
590
641
|
```
|
|
591
642
|
|
|
592
643
|
**Display Mode Behavior:**
|
|
593
|
-
- **`pills`**: Individual removable
|
|
594
|
-
- **`count`**: Shows "X selected" text with clear button. Calls `
|
|
595
|
-
- **`compact`**: Shows first item + count in single
|
|
596
|
-
- **`partial`**: Shows first N
|
|
597
|
-
- **`none`**: No display in
|
|
644
|
+
- **`pills`**: Individual removable badges for each selected item. Calls `getBadgeDisplayCallback` for each item.
|
|
645
|
+
- **`count`**: Shows "X selected" text with clear button. Calls `getCounterCallback(count)`.
|
|
646
|
+
- **`compact`**: Shows first item + count in single badge (e.g., "JavaScript (+2 more)"). Calls `getBadgeDisplayCallback(firstItem)` and `getCounterCallback(count, remainingCount)`.
|
|
647
|
+
- **`partial`**: Shows first N badges + "+X more" badge. Calls `getBadgeDisplayCallback` for visible items and `getCounterCallback(count, remainingCount)` for badge.
|
|
648
|
+
- **`none`**: No display in badges area. No callbacks invoked. Use with `show-counter="true"` for minimal UI.
|
|
649
|
+
|
|
650
|
+
**Badge Styling:**
|
|
651
|
+
- **Data badges** (selected items like "JavaScript", "Python"): Blue styling by default
|
|
652
|
+
- **BadgeCounters** ("+3 more", "5 selected", compact mode display): Gray styling to distinguish from data
|
|
653
|
+
- Both can be customized via CSS variables (see `--ml-badge-*` and `--ml-badge-counter-*`)
|
|
598
654
|
|
|
599
|
-
**
|
|
655
|
+
**Counter (`show-counter="true"`)**: Independent feature showing `[X]` next to toggle icon. Works with all display modes. Not affected by callbacks.
|
|
600
656
|
|
|
601
|
-
###
|
|
657
|
+
### Badge Positioning
|
|
602
658
|
|
|
603
659
|
Control where selected item badges appear relative to the input:
|
|
604
660
|
|
|
605
661
|
```html
|
|
606
|
-
<!--
|
|
607
|
-
<web-multiselect
|
|
662
|
+
<!-- Badges below input (default) -->
|
|
663
|
+
<web-multiselect badges-position="bottom"></web-multiselect>
|
|
608
664
|
|
|
609
|
-
<!--
|
|
610
|
-
<web-multiselect
|
|
665
|
+
<!-- Badges above input -->
|
|
666
|
+
<web-multiselect badges-position="top"></web-multiselect>
|
|
611
667
|
|
|
612
|
-
<!--
|
|
613
|
-
<web-multiselect
|
|
668
|
+
<!-- Badges to the left of input -->
|
|
669
|
+
<web-multiselect badges-position="left"></web-multiselect>
|
|
614
670
|
|
|
615
|
-
<!--
|
|
616
|
-
<web-multiselect
|
|
671
|
+
<!-- Badges to the right of input -->
|
|
672
|
+
<web-multiselect badges-position="right"></web-multiselect>
|
|
617
673
|
```
|
|
618
674
|
|
|
619
|
-
**Note:** In RTL mode, left/right positions are automatically mirrored - `
|
|
675
|
+
**Note:** In RTL mode, left/right positions are automatically mirrored - `badges-position="left"` will appear on the physical right side in RTL languages.
|
|
620
676
|
|
|
621
|
-
###
|
|
677
|
+
### Badge Tooltips
|
|
622
678
|
|
|
623
|
-
Enable tooltips on selected item
|
|
679
|
+
Enable tooltips on selected item badges with customizable placement and delay:
|
|
624
680
|
|
|
625
681
|
```html
|
|
626
682
|
<!-- Basic tooltips -->
|
|
627
683
|
<web-multiselect
|
|
628
|
-
enable-
|
|
629
|
-
|
|
684
|
+
enable-badge-tooltips="true"
|
|
685
|
+
badge-tooltip-placement="top">
|
|
630
686
|
</web-multiselect>
|
|
631
687
|
|
|
632
688
|
<!-- Fast tooltips with custom delay -->
|
|
633
689
|
<web-multiselect
|
|
634
|
-
enable-
|
|
635
|
-
|
|
690
|
+
enable-badge-tooltips="true"
|
|
691
|
+
badge-tooltip-delay="100">
|
|
636
692
|
</web-multiselect>
|
|
637
693
|
|
|
638
694
|
<script type="module">
|
|
639
695
|
const select = document.querySelector('web-multiselect');
|
|
640
696
|
|
|
641
697
|
// Custom tooltip content
|
|
642
|
-
select.
|
|
698
|
+
select.getBadgeTooltipCallback = (item) => {
|
|
643
699
|
return `${item.label} - ${item.subtitle}`;
|
|
644
700
|
};
|
|
645
701
|
</script>
|
|
@@ -647,21 +703,21 @@ Enable tooltips on selected item pills with customizable placement and delay:
|
|
|
647
703
|
|
|
648
704
|
### Internationalization (i18n)
|
|
649
705
|
|
|
650
|
-
Customize
|
|
706
|
+
Customize counter text for proper pluralization and localization:
|
|
651
707
|
|
|
652
708
|
```html
|
|
653
709
|
<web-multiselect
|
|
654
710
|
id="i18n-select"
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
711
|
+
badges-threshold="5"
|
|
712
|
+
badges-threshold-mode="partial"
|
|
713
|
+
badges-max-visible="3">
|
|
658
714
|
</web-multiselect>
|
|
659
715
|
|
|
660
716
|
<script type="module">
|
|
661
717
|
const select = document.getElementById('i18n-select');
|
|
662
718
|
|
|
663
719
|
// Spanish pluralization example
|
|
664
|
-
select.
|
|
720
|
+
select.getCounterCallback = (count, moreCount) => {
|
|
665
721
|
if (moreCount !== undefined) {
|
|
666
722
|
// Partial mode: "+X more" badge
|
|
667
723
|
return moreCount === 1 ? '+1 más' : `+${moreCount} más`;
|
|
@@ -693,12 +749,419 @@ Full RTL support for Arabic, Hebrew, Persian, Urdu, and other right-to-left lang
|
|
|
693
749
|
|
|
694
750
|
**RTL Features:**
|
|
695
751
|
- ✅ **Auto-detection** - Detects `dir="rtl"` on component or any ancestor element
|
|
696
|
-
- ✅ **Complete UI mirroring** - Toggle icon, text alignment,
|
|
697
|
-
- ✅ **Logical positioning** - `
|
|
698
|
-
- ✅ **
|
|
752
|
+
- ✅ **Complete UI mirroring** - Toggle icon, text alignment, badges, dropdown, badges
|
|
753
|
+
- ✅ **Logical positioning** - `badges-position="left"` becomes physically right in RTL
|
|
754
|
+
- ✅ **Badge remove buttons** - Flip to left side in RTL mode
|
|
699
755
|
- ✅ **Text direction** - All text content properly right-aligned
|
|
700
756
|
- ✅ **No configuration needed** - Just set `dir="rtl"` attribute
|
|
701
757
|
|
|
758
|
+
### Custom Rendering
|
|
759
|
+
|
|
760
|
+
The component provides powerful custom rendering callbacks that allow you to fully customize how options, badges, and selected items are displayed while maintaining the component's structure and functionality.
|
|
761
|
+
|
|
762
|
+
#### Overview
|
|
763
|
+
|
|
764
|
+
Three rendering callbacks are available:
|
|
765
|
+
- **`renderOptionContentCallback`** - Customize dropdown option content
|
|
766
|
+
- **`renderBadgeContentCallback`** - Customize badge (selected item) content
|
|
767
|
+
- **`renderSelectedContentCallback`** - Customize selected value text (single-select mode)
|
|
768
|
+
|
|
769
|
+
All callbacks can return either **HTML strings** or **HTMLElement** objects (except `renderSelectedContentCallback` which returns plain text).
|
|
770
|
+
|
|
771
|
+
#### Custom Option Rendering
|
|
772
|
+
|
|
773
|
+
Customize how options appear in the dropdown:
|
|
774
|
+
|
|
775
|
+
```html
|
|
776
|
+
<web-multiselect id="custom-options"></web-multiselect>
|
|
777
|
+
|
|
778
|
+
<script type="module">
|
|
779
|
+
import '@keenmate/web-multiselect';
|
|
780
|
+
|
|
781
|
+
const select = document.getElementById('custom-options');
|
|
782
|
+
|
|
783
|
+
select.options = [
|
|
784
|
+
{ id: 1, name: 'React', stars: 220000, trending: true },
|
|
785
|
+
{ id: 2, name: 'Vue', stars: 207000, trending: false },
|
|
786
|
+
{ id: 3, name: 'Angular', stars: 94000, trending: false },
|
|
787
|
+
{ id: 4, name: 'Svelte', stars: 76000, trending: true }
|
|
788
|
+
];
|
|
789
|
+
|
|
790
|
+
// Custom renderer with full HTML control
|
|
791
|
+
select.renderOptionContentCallback = (item, context) => {
|
|
792
|
+
// Context provides: { index, isSelected, isFocused, isMatched, isDisabled }
|
|
793
|
+
|
|
794
|
+
return `
|
|
795
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
796
|
+
<strong>${item.name}</strong>
|
|
797
|
+
<span style="color: #666; font-size: 0.875rem;">⭐ ${(item.stars / 1000).toFixed(0)}k</span>
|
|
798
|
+
${item.trending ? '<span style="background: #10b981; color: white; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.75rem;">🔥 Trending</span>' : ''}
|
|
799
|
+
</div>
|
|
800
|
+
`;
|
|
801
|
+
};
|
|
802
|
+
</script>
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
**Context object** (`OptionContentRenderContext`):
|
|
806
|
+
- `index: number` - Index of the option in the filtered list
|
|
807
|
+
- `isSelected: boolean` - Whether the option is currently selected
|
|
808
|
+
- `isFocused: boolean` - Whether the option is currently focused (keyboard navigation)
|
|
809
|
+
- `isMatched: boolean` - Whether the option matches the current search term (navigate mode only)
|
|
810
|
+
- `isDisabled: boolean` - Whether the option is disabled
|
|
811
|
+
|
|
812
|
+
#### Custom Badge Rendering
|
|
813
|
+
|
|
814
|
+
Customize how selected items appear as badges:
|
|
815
|
+
|
|
816
|
+
```javascript
|
|
817
|
+
const select = document.querySelector('web-multiselect');
|
|
818
|
+
|
|
819
|
+
select.options = [
|
|
820
|
+
{ id: 1, name: 'John Doe', role: 'Admin', avatar: '👨💼' },
|
|
821
|
+
{ id: 2, name: 'Jane Smith', role: 'Developer', avatar: '👩💻' },
|
|
822
|
+
{ id: 3, name: 'Bob Johnson', role: 'Designer', avatar: '🎨' }
|
|
823
|
+
];
|
|
824
|
+
|
|
825
|
+
// Custom badge rendering in main badges area
|
|
826
|
+
select.renderBadgeContentCallback = (item, context) => {
|
|
827
|
+
// Compact view in badges area
|
|
828
|
+
return `${item.avatar} ${item.name}`;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
// Custom rendering for selected items popover (separate callback)
|
|
832
|
+
select.renderSelectionBadgeContentCallback = (item) => {
|
|
833
|
+
// Full details in popover - has more space
|
|
834
|
+
return `
|
|
835
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
836
|
+
<span>${item.avatar}</span>
|
|
837
|
+
<div>
|
|
838
|
+
<div><strong>${item.name}</strong></div>
|
|
839
|
+
<div style="font-size: 0.75rem; color: #666;">${item.role}</div>
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
842
|
+
`;
|
|
843
|
+
};
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
**Separate Callbacks for Badges vs. Popover:**
|
|
847
|
+
- `renderBadgeContentCallback` - Renders badges in the main badges area (compact display)
|
|
848
|
+
- `renderSelectionBadgeContentCallback` - Renders items in the selected items popover (can be more detailed)
|
|
849
|
+
- If `renderSelectionBadgeContentCallback` is not defined, falls back to `renderBadgeContentCallback`
|
|
850
|
+
- Users can assign the same function to both if identical rendering is desired
|
|
851
|
+
|
|
852
|
+
**Context object** (`BadgeContentRenderContext` for `renderBadgeContentCallback`):
|
|
853
|
+
- `displayMode: BadgesDisplayMode` - Current badges display mode ('pills', 'count', 'compact', 'partial', 'none')
|
|
854
|
+
- `isInPopover: boolean` - Whether the badge is being rendered in the selected items popover (always false for this callback)
|
|
855
|
+
|
|
856
|
+
#### Custom Badge Styling with CSS Classes
|
|
857
|
+
|
|
858
|
+
Add custom CSS classes to badges based on item data for semantic styling:
|
|
859
|
+
|
|
860
|
+
```javascript
|
|
861
|
+
const select = document.querySelector('web-multiselect');
|
|
862
|
+
|
|
863
|
+
select.options = [
|
|
864
|
+
{ id: 1, task: 'Fix security bug', priority: 'urgent' },
|
|
865
|
+
{ id: 2, task: 'Update docs', priority: 'normal' },
|
|
866
|
+
{ id: 3, task: 'Refactor code', priority: 'low' }
|
|
867
|
+
];
|
|
868
|
+
|
|
869
|
+
// Add CSS class based on priority
|
|
870
|
+
select.getBadgeClassCallback = (item) => {
|
|
871
|
+
return `badge-${item.priority}`; // Returns 'badge-urgent', 'badge-normal', etc.
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
// Can also return array of classes
|
|
875
|
+
select.getBadgeClassCallback = (item) => {
|
|
876
|
+
const classes = [`badge-${item.priority}`];
|
|
877
|
+
if (item.urgent) classes.push('badge-blink');
|
|
878
|
+
return classes;
|
|
879
|
+
};
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
Then style with CSS:
|
|
883
|
+
|
|
884
|
+
```css
|
|
885
|
+
/* Target specific badges with custom classes */
|
|
886
|
+
.badge-urgent {
|
|
887
|
+
--ml-badge-text-bg: #fee2e2;
|
|
888
|
+
--ml-badge-text-color: #dc2626;
|
|
889
|
+
--ml-badge-remove-bg: #dc2626;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.badge-normal {
|
|
893
|
+
--ml-badge-text-bg: #dbeafe;
|
|
894
|
+
--ml-badge-text-color: #2563eb;
|
|
895
|
+
--ml-badge-remove-bg: #2563eb;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.badge-low {
|
|
899
|
+
--ml-badge-text-bg: #d1fae5;
|
|
900
|
+
--ml-badge-text-color: #059669;
|
|
901
|
+
--ml-badge-remove-bg: #059669;
|
|
902
|
+
}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
The callback:
|
|
906
|
+
- Takes the item as a parameter
|
|
907
|
+
- Returns a string (single class) or array of strings (multiple classes)
|
|
908
|
+
- Classes are added to the badge's base `.ml__badge` element
|
|
909
|
+
- Works across all rendering locations (main badges, partial mode, popover)
|
|
910
|
+
|
|
911
|
+
**Separate Class Callbacks for Badges vs. Popover:**
|
|
912
|
+
|
|
913
|
+
Similar to rendering callbacks, you can use different class callbacks for badges and selected items:
|
|
914
|
+
|
|
915
|
+
```javascript
|
|
916
|
+
// Add classes to badges in main area
|
|
917
|
+
select.getBadgeClassCallback = (item) => {
|
|
918
|
+
return `badge-${item.priority}`;
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// Add different/additional classes to selected items in popover
|
|
922
|
+
select.getSelectionBadgeClassCallback = (item) => {
|
|
923
|
+
// Could add more detailed classes for popover items
|
|
924
|
+
return [`badge-${item.priority}`, 'badge-detailed'];
|
|
925
|
+
};
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
- `getBadgeClassCallback` - Adds classes to badges in the main badges area
|
|
929
|
+
- `getSelectionBadgeClassCallback` - Adds classes to items in the selected items popover
|
|
930
|
+
- If `getSelectionBadgeClassCallback` is not defined, falls back to `getBadgeClassCallback`
|
|
931
|
+
- Users can assign the same function to both if identical styling is desired
|
|
932
|
+
|
|
933
|
+
**Shadow DOM CSS Injection:**
|
|
934
|
+
|
|
935
|
+
Since the component uses Shadow DOM, regular page CSS cannot style shadow elements. Use `customStylesCallback` to inject CSS directly into the Shadow DOM:
|
|
936
|
+
|
|
937
|
+
```javascript
|
|
938
|
+
const select = document.querySelector('web-multiselect');
|
|
939
|
+
|
|
940
|
+
// Add CSS classes to badges based on item data
|
|
941
|
+
select.getBadgeClassCallback = (item) => {
|
|
942
|
+
return `badge-${item.priority}`;
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
// Inject CSS into Shadow DOM to style those classes
|
|
946
|
+
select.customStylesCallback = () => `
|
|
947
|
+
.badge-urgent {
|
|
948
|
+
--ml-badge-text-bg: #fee2e2;
|
|
949
|
+
--ml-badge-text-color: #dc2626;
|
|
950
|
+
--ml-badge-remove-bg: #dc2626;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.badge-normal {
|
|
954
|
+
--ml-badge-text-bg: #dbeafe;
|
|
955
|
+
--ml-badge-text-color: #2563eb;
|
|
956
|
+
--ml-badge-remove-bg: #2563eb;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.badge-low {
|
|
960
|
+
--ml-badge-text-bg: #d1fae5;
|
|
961
|
+
--ml-badge-text-color: #059669;
|
|
962
|
+
--ml-badge-remove-bg: #059669;
|
|
963
|
+
}
|
|
964
|
+
`;
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
The `customStylesCallback`:
|
|
968
|
+
- Returns a CSS string (not HTML)
|
|
969
|
+
- Styles are injected into the Shadow DOM on initialization
|
|
970
|
+
- Can be updated dynamically - new styles replace old ones
|
|
971
|
+
- Works with all custom classes (from `getBadgeClassCallback`, `renderOptionContentCallback`, etc.)
|
|
972
|
+
|
|
973
|
+
#### Custom Selected Item Rendering (Single-Select)
|
|
974
|
+
|
|
975
|
+
Customize the text shown in the input field when in single-select mode:
|
|
976
|
+
|
|
977
|
+
```javascript
|
|
978
|
+
const select = document.querySelector('web-multiselect[multiple="false"]');
|
|
979
|
+
|
|
980
|
+
select.options = [
|
|
981
|
+
{ id: 1, firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
|
|
982
|
+
{ id: 2, firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }
|
|
983
|
+
];
|
|
984
|
+
|
|
985
|
+
// Show just first name when closed
|
|
986
|
+
select.renderSelectedContentCallback = (item) => {
|
|
987
|
+
return item.firstName; // Returns plain text (not HTML)
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
// While dropdown shows full details
|
|
991
|
+
select.getDisplayValueCallback = (item) => {
|
|
992
|
+
return `${item.firstName} ${item.lastName} (${item.email})`;
|
|
993
|
+
};
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
#### Conditional Rendering Example
|
|
997
|
+
|
|
998
|
+
Use JavaScript logic for conditional rendering:
|
|
999
|
+
|
|
1000
|
+
```javascript
|
|
1001
|
+
select.renderOptionContentCallback = (item, context) => {
|
|
1002
|
+
const classes = [];
|
|
1003
|
+
if (context.isSelected) classes.push('selected');
|
|
1004
|
+
if (context.isFocused) classes.push('focused');
|
|
1005
|
+
|
|
1006
|
+
return `
|
|
1007
|
+
<div class="${classes.join(' ')}">
|
|
1008
|
+
${item.isNew ? '<span class="badge-new">NEW</span>' : ''}
|
|
1009
|
+
<strong>${item.name}</strong>
|
|
1010
|
+
${item.description ? `<p style="font-size: 0.875rem; color: #666;">${item.description}</p>` : ''}
|
|
1011
|
+
${item.tags ? `<div class="tags">${item.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}</div>` : ''}
|
|
1012
|
+
</div>
|
|
1013
|
+
`;
|
|
1014
|
+
};
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
#### Returning HTMLElement
|
|
1018
|
+
|
|
1019
|
+
You can also return DOM elements for more complex rendering:
|
|
1020
|
+
|
|
1021
|
+
```javascript
|
|
1022
|
+
select.renderOptionContentCallback = (item, context) => {
|
|
1023
|
+
const div = document.createElement('div');
|
|
1024
|
+
div.style.display = 'flex';
|
|
1025
|
+
div.style.alignItems = 'center';
|
|
1026
|
+
div.style.gap = '0.5rem';
|
|
1027
|
+
|
|
1028
|
+
const img = document.createElement('img');
|
|
1029
|
+
img.src = item.avatarUrl;
|
|
1030
|
+
img.style.width = '32px';
|
|
1031
|
+
img.style.height = '32px';
|
|
1032
|
+
img.style.borderRadius = '50%';
|
|
1033
|
+
|
|
1034
|
+
const span = document.createElement('span');
|
|
1035
|
+
span.textContent = item.name;
|
|
1036
|
+
|
|
1037
|
+
div.appendChild(img);
|
|
1038
|
+
div.appendChild(span);
|
|
1039
|
+
|
|
1040
|
+
return div; // Return HTMLElement instead of string
|
|
1041
|
+
};
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
#### Virtual Scroll Compatibility
|
|
1045
|
+
|
|
1046
|
+
When using `renderOptionContentCallback` with virtual scroll enabled:
|
|
1047
|
+
|
|
1048
|
+
⚠️ **Important**: Custom option content **must fit within** the configured `optionHeight` (default: 50px)
|
|
1049
|
+
|
|
1050
|
+
```html
|
|
1051
|
+
<web-multiselect
|
|
1052
|
+
id="large-dataset"
|
|
1053
|
+
enable-virtual-scroll="true"
|
|
1054
|
+
option-height="60">
|
|
1055
|
+
</web-multiselect>
|
|
1056
|
+
|
|
1057
|
+
<script type="module">
|
|
1058
|
+
const select = document.getElementById('large-dataset');
|
|
1059
|
+
|
|
1060
|
+
select.renderOptionContentCallback = (item) => {
|
|
1061
|
+
// Content must fit in 60px height
|
|
1062
|
+
return `
|
|
1063
|
+
<div style="height: 60px; display: flex; align-items: center;">
|
|
1064
|
+
<strong>${item.name}</strong>
|
|
1065
|
+
</div>
|
|
1066
|
+
`;
|
|
1067
|
+
};
|
|
1068
|
+
</script>
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
**Virtual scroll requirements:**
|
|
1072
|
+
- Content height must be **fixed** and match `optionHeight`
|
|
1073
|
+
- Overflow will be clipped
|
|
1074
|
+
- Variable-height content only works in non-virtual mode
|
|
1075
|
+
|
|
1076
|
+
#### Callback Priority
|
|
1077
|
+
|
|
1078
|
+
The component uses a fallback chain when callbacks are not provided:
|
|
1079
|
+
|
|
1080
|
+
**For options:**
|
|
1081
|
+
1. `renderOptionContentCallback` (full HTML control)
|
|
1082
|
+
2. Default: icon + `getDisplayValueCallback` + subtitle
|
|
1083
|
+
|
|
1084
|
+
**For badges:**
|
|
1085
|
+
1. `renderBadgeContentCallback` (full HTML control)
|
|
1086
|
+
2. `getBadgeDisplayCallback` (text only)
|
|
1087
|
+
3. `getDisplayValueCallback` (text only)
|
|
1088
|
+
|
|
1089
|
+
**For selected item (single-select):**
|
|
1090
|
+
1. `renderSelectedContentCallback` (text only)
|
|
1091
|
+
2. `getDisplayValueCallback` (text only)
|
|
1092
|
+
|
|
1093
|
+
#### Checkbox Control
|
|
1094
|
+
|
|
1095
|
+
Control checkbox appearance and alignment with CSS variables and attributes:
|
|
1096
|
+
|
|
1097
|
+
**Checkbox Alignment (via attribute):**
|
|
1098
|
+
```html
|
|
1099
|
+
<web-multiselect checkbox-align="top"></web-multiselect> <!-- Default -->
|
|
1100
|
+
<web-multiselect checkbox-align="center"></web-multiselect> <!-- Middle aligned -->
|
|
1101
|
+
<web-multiselect checkbox-align="bottom"></web-multiselect> <!-- Bottom aligned -->
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
**Checkbox Size/Scale (via CSS):**
|
|
1105
|
+
```html
|
|
1106
|
+
<style>
|
|
1107
|
+
/* Change checkbox size */
|
|
1108
|
+
web-multiselect {
|
|
1109
|
+
--ml-checkbox-size: 20px; /* Width and height (default: 16px) */
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/* Scale checkbox */
|
|
1113
|
+
web-multiselect {
|
|
1114
|
+
--ml-checkbox-scale: 1.5; /* Scale multiplier (default: 1) */
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/* Fine-tune vertical position */
|
|
1118
|
+
web-multiselect {
|
|
1119
|
+
--ml-checkbox-margin-top: 0.5rem; /* Offset from top (default: 0.125rem) */
|
|
1120
|
+
}
|
|
1121
|
+
</style>
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
**CSS Grid/Flexbox in Custom Content:**
|
|
1125
|
+
|
|
1126
|
+
Custom rendering callbacks support full CSS layout control:
|
|
1127
|
+
|
|
1128
|
+
```javascript
|
|
1129
|
+
// CSS Grid example
|
|
1130
|
+
multiselect.renderOptionContentCallback = (item, context) => {
|
|
1131
|
+
return `
|
|
1132
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
|
|
1133
|
+
<div><strong>Name:</strong> ${item.name}</div>
|
|
1134
|
+
<div><strong>Price:</strong> ${item.price}</div>
|
|
1135
|
+
<div><strong>Stock:</strong> ${item.stock}</div>
|
|
1136
|
+
<div><strong>Rating:</strong> ${item.rating}</div>
|
|
1137
|
+
</div>
|
|
1138
|
+
`;
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
// Flexbox example
|
|
1142
|
+
multiselect.renderOptionContentCallback = (item, context) => {
|
|
1143
|
+
return `
|
|
1144
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
1145
|
+
<div style="display: flex; flex-direction: column;">
|
|
1146
|
+
<strong>${item.name}</strong>
|
|
1147
|
+
<span style="font-size: 0.875rem; color: #666;">${item.description}</span>
|
|
1148
|
+
</div>
|
|
1149
|
+
<div style="text-align: right;">
|
|
1150
|
+
<div>${item.price}</div>
|
|
1151
|
+
<div style="font-size: 0.875rem;">${item.stock} in stock</div>
|
|
1152
|
+
</div>
|
|
1153
|
+
</div>
|
|
1154
|
+
`;
|
|
1155
|
+
};
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
**Available CSS Variables:**
|
|
1159
|
+
- `--ml-checkbox-size`: Checkbox width/height (default: `16px`)
|
|
1160
|
+
- `--ml-checkbox-scale`: Scale multiplier (default: `1`)
|
|
1161
|
+
- `--ml-checkbox-margin-top`: Vertical offset (default: `0.125rem`)
|
|
1162
|
+
- `--ml-checkbox-align`: Alignment value (default: `flex-start`)
|
|
1163
|
+
- `--ml-option-gap`: Gap between checkbox and content (default: `0.5rem`)
|
|
1164
|
+
|
|
702
1165
|
### Flexible Data Handling
|
|
703
1166
|
|
|
704
1167
|
The component supports **any data structure** through a member/callback pattern, allowing you to work with custom objects, tuple arrays, or existing API responses without transformation.
|
|
@@ -773,9 +1236,9 @@ select.getDisabledCallback = (item) => {
|
|
|
773
1236
|
return item.stock === 0 || item.discontinued;
|
|
774
1237
|
};
|
|
775
1238
|
|
|
776
|
-
// Customize
|
|
777
|
-
select.
|
|
778
|
-
//
|
|
1239
|
+
// Customize badge display (show different text in badges vs dropdown)
|
|
1240
|
+
select.getBadgeDisplayCallback = (item) => {
|
|
1241
|
+
// Badges show just the name for space efficiency
|
|
779
1242
|
return item.name;
|
|
780
1243
|
// While dropdown can show full details: "Laptop - $999 - Electronics"
|
|
781
1244
|
};
|
|
@@ -1085,30 +1548,32 @@ For the complete list of all available CSS variables, see:
|
|
|
1085
1548
|
| `--ml-option-hover-bg` | `#f9fafb` | Option background on hover |
|
|
1086
1549
|
| `--ml-option-bg-selected` | (rgba accent) | Selected option background |
|
|
1087
1550
|
|
|
1088
|
-
####
|
|
1551
|
+
#### Badges
|
|
1089
1552
|
|
|
1090
1553
|
| Variable | Default | Description |
|
|
1091
1554
|
|----------|---------|-------------|
|
|
1092
|
-
| `--ml-
|
|
1093
|
-
| `--ml-
|
|
1094
|
-
| `--ml-
|
|
1095
|
-
| `--ml-
|
|
1096
|
-
| `--ml-
|
|
1097
|
-
| `--ml-
|
|
1098
|
-
| `--ml-
|
|
1099
|
-
| `--ml-
|
|
1100
|
-
| `--ml-
|
|
1101
|
-
| `--ml-
|
|
1102
|
-
| `--ml-
|
|
1103
|
-
|
|
1104
|
-
|
|
1555
|
+
| `--ml-badge-text-bg` | `#eff6ff` | Badge background color |
|
|
1556
|
+
| `--ml-badge-text-color` | `#3b82f6` | Badge text color |
|
|
1557
|
+
| `--ml-badge-gap` | `0.5rem` | Gap between badges |
|
|
1558
|
+
| `--ml-badge-height` | `1.5rem` | Height of badges |
|
|
1559
|
+
| `--ml-badge-font-size` | `0.75rem` | Badge font size |
|
|
1560
|
+
| `--ml-badge-border-radius` | `0.375rem` | Badge border radius |
|
|
1561
|
+
| `--ml-badge-remove-bg` | `#3b82f6` | Remove button background |
|
|
1562
|
+
| `--ml-badge-remove-color` | `#ffffff` | Remove button color |
|
|
1563
|
+
| `--ml-badge-counter-text-bg` | `#d1d5db` | BadgeCounter text background ("+X more") |
|
|
1564
|
+
| `--ml-badge-counter-text-color` | `#6b7280` | BadgeCounter text color |
|
|
1565
|
+
| `--ml-badge-counter-remove-bg` | `#6b7280` | BadgeCounter remove button background |
|
|
1566
|
+
| `--ml-badge-counter-remove-color` | `#ffffff` | BadgeCounter remove button color |
|
|
1567
|
+
| `--ml-badge-counter-border` | `1px solid #e5e7eb` | BadgeCounter border |
|
|
1568
|
+
|
|
1569
|
+
#### Counter (in input)
|
|
1105
1570
|
|
|
1106
1571
|
| Variable | Default | Description |
|
|
1107
1572
|
|----------|---------|-------------|
|
|
1108
|
-
| `--ml-
|
|
1109
|
-
| `--ml-
|
|
1110
|
-
| `--ml-
|
|
1111
|
-
| `--ml-
|
|
1573
|
+
| `--ml-counter-bg` | `#3b82f6` | Counter background |
|
|
1574
|
+
| `--ml-counter-color` | `#ffffff` | Counter text color |
|
|
1575
|
+
| `--ml-counter-font-size` | `0.75rem` | Counter font size |
|
|
1576
|
+
| `--ml-counter-bg-hover` | `#2563eb` | Hover background color |
|
|
1112
1577
|
|
|
1113
1578
|
#### Tooltips
|
|
1114
1579
|
|