@keenmate/web-multiselect 1.12.0-rc02 → 1.12.0-rc03
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 +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/multiselect.js +376 -350
- package/dist/multiselect.umd.js +13 -13
- package/docs/accessibility.md +84 -0
- package/docs/examples.md +1180 -0
- package/docs/theming.md +388 -0
- package/docs/usage.md +307 -0
- package/package.json +5 -1
package/docs/examples.md
ADDED
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
# Examples & cookbook
|
|
2
|
+
|
|
3
|
+
Working examples and recipes for `@keenmate/web-multiselect`. For the runnable demos, open any of the HTML files at the package root:
|
|
4
|
+
|
|
5
|
+
| File | What it shows |
|
|
6
|
+
|------|---------------|
|
|
7
|
+
| [`examples-classic.html`](../examples-classic.html) | Baseline declarative + programmatic usage |
|
|
8
|
+
| [`examples-new-api.html`](../examples-new-api.html) | Modern programmatic API |
|
|
9
|
+
| [`examples-templating.html`](../examples-templating.html) | Custom rendering callbacks |
|
|
10
|
+
| [`examples-action-buttons.html`](../examples-action-buttons.html) | Select All / Clear All / custom actions |
|
|
11
|
+
| [`examples-base-variables.html`](../examples-base-variables.html) | `--base-*` two-layer theming |
|
|
12
|
+
| [`examples-theming.html`](../examples-theming.html) | Full `--ms-*` theming surface |
|
|
13
|
+
| [`examples-sizes.html`](../examples-sizes.html) | Size variants and `--ms-rem` scaling |
|
|
14
|
+
| [`examples-positioning.html`](../examples-positioning.html) | Floating UI positioning edge cases |
|
|
15
|
+
| [`examples-performance.html`](../examples-performance.html) | Virtual scroll with 15,000 items |
|
|
16
|
+
| [`examples-logging.html`](../examples-logging.html) | Debug logging |
|
|
17
|
+
|
|
18
|
+
## Rich content with icons
|
|
19
|
+
|
|
20
|
+
Icons support multiple formats — emojis, SVG markup, Font Awesome, images, or any HTML:
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<web-multiselect id="frameworks"></web-multiselect>
|
|
24
|
+
|
|
25
|
+
<script type="module">
|
|
26
|
+
const select = document.getElementById('frameworks');
|
|
27
|
+
select.options = [
|
|
28
|
+
{
|
|
29
|
+
value: 'react',
|
|
30
|
+
label: 'React',
|
|
31
|
+
icon: '⚛️', // Emoji
|
|
32
|
+
subtitle: 'A JavaScript library for building user interfaces'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: 'vue',
|
|
36
|
+
label: 'Vue.js',
|
|
37
|
+
icon: '<svg viewBox="0 0 24 24"><path d="M2 3l10 18L22 3h-4l-6 10.5L6 3H2z"/></svg>', // SVG
|
|
38
|
+
subtitle: 'The Progressive JavaScript Framework'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
value: 'angular',
|
|
42
|
+
label: 'Angular',
|
|
43
|
+
icon: '<i class="fab fa-angular"></i>', // Font Awesome
|
|
44
|
+
subtitle: 'Platform for building mobile and desktop apps'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: 'svelte',
|
|
48
|
+
label: 'Svelte',
|
|
49
|
+
icon: '<img src="svelte-logo.png" alt="Svelte" />', // Image
|
|
50
|
+
subtitle: 'Cybernetically enhanced web apps'
|
|
51
|
+
}
|
|
52
|
+
];
|
|
53
|
+
</script>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Grouped options
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
select.options = [
|
|
60
|
+
{ value: 'js', label: 'JavaScript', group: 'Frontend' },
|
|
61
|
+
{ value: 'ts', label: 'TypeScript', group: 'Frontend' },
|
|
62
|
+
{ value: 'python', label: 'Python', group: 'Backend' },
|
|
63
|
+
{ value: 'java', label: 'Java', group: 'Backend' }
|
|
64
|
+
];
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Async data loading
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<web-multiselect
|
|
71
|
+
id="async-select"
|
|
72
|
+
min-search-length="2"
|
|
73
|
+
loading-message="Searching..."
|
|
74
|
+
empty-message="No products found">
|
|
75
|
+
</web-multiselect>
|
|
76
|
+
|
|
77
|
+
<script type="module">
|
|
78
|
+
const select = document.getElementById('async-select');
|
|
79
|
+
|
|
80
|
+
select.onSearch = async (searchTerm) => {
|
|
81
|
+
const response = await fetch(`/api/products?q=${searchTerm}`);
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
return data.products;
|
|
84
|
+
};
|
|
85
|
+
</script>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Hybrid static + dynamic search
|
|
89
|
+
|
|
90
|
+
Show popular items initially, then switch to full database search when the user types. Perfect for showing "Top 10" items while supporting comprehensive search:
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<web-multiselect
|
|
94
|
+
id="hybrid-select"
|
|
95
|
+
min-search-length="3"
|
|
96
|
+
keep-options-on-search="true">
|
|
97
|
+
</web-multiselect>
|
|
98
|
+
|
|
99
|
+
<script type="module">
|
|
100
|
+
const select = document.getElementById('hybrid-select');
|
|
101
|
+
|
|
102
|
+
// Set initial popular items (shown when dropdown opens)
|
|
103
|
+
select.options = [
|
|
104
|
+
{ id: 1, name: 'React' },
|
|
105
|
+
{ id: 2, name: 'Vue' },
|
|
106
|
+
{ id: 3, name: 'Angular' },
|
|
107
|
+
{ id: 4, name: 'Svelte' },
|
|
108
|
+
{ id: 5, name: 'Solid' }
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// Pre-process search terms (remove accents, validate, etc.)
|
|
112
|
+
select.beforeSearchCallback = (searchTerm) => {
|
|
113
|
+
const normalized = searchTerm.normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
114
|
+
if (normalized.length < 2) return null;
|
|
115
|
+
return normalized;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Search full database when user types 3+ characters
|
|
119
|
+
select.onSearch = async (searchTerm) => {
|
|
120
|
+
const response = await fetch(`/api/frameworks/search?q=${searchTerm}`);
|
|
121
|
+
return await response.json();
|
|
122
|
+
};
|
|
123
|
+
</script>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**How it works:**
|
|
127
|
+
1. **Dropdown opens** → shows 5 popular frameworks.
|
|
128
|
+
2. **User types "rea"** → calls API, shows all matching results from database.
|
|
129
|
+
3. **User clears search** → shows 5 popular frameworks again.
|
|
130
|
+
4. **User types "café"** → `beforeSearchCallback` converts to "cafe", then searches.
|
|
131
|
+
|
|
132
|
+
**Key options:**
|
|
133
|
+
- `keep-options-on-search="true"` (default) — keep initial options visible when search is empty/short.
|
|
134
|
+
- `beforeSearchCallback` — transform search text or block search by returning `null`.
|
|
135
|
+
- `min-search-length` — minimum characters before triggering search (shows initial options below this).
|
|
136
|
+
|
|
137
|
+
## Virtual scrolling for large datasets
|
|
138
|
+
|
|
139
|
+
Handle 10,000+ options with smooth 60fps performance by rendering only visible items:
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<web-multiselect
|
|
143
|
+
id="large-dataset"
|
|
144
|
+
enable-virtual-scroll="true"
|
|
145
|
+
virtual-scroll-threshold="100"
|
|
146
|
+
option-height="50"
|
|
147
|
+
virtual-scroll-buffer="10"
|
|
148
|
+
search-mode="filter"
|
|
149
|
+
max-height="400px">
|
|
150
|
+
</web-multiselect>
|
|
151
|
+
|
|
152
|
+
<script type="module">
|
|
153
|
+
import '@keenmate/web-multiselect';
|
|
154
|
+
|
|
155
|
+
const select = document.getElementById('large-dataset');
|
|
156
|
+
|
|
157
|
+
// Generate 15,000 options
|
|
158
|
+
const largeDataset = Array.from({ length: 15000 }, (_, i) => ({
|
|
159
|
+
value: i,
|
|
160
|
+
label: `Item ${i.toString().padStart(5, '0')}`
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
select.options = largeDataset;
|
|
164
|
+
</script>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Performance comparison (15,000 items):**
|
|
168
|
+
|
|
169
|
+
| Metric | Without virtual scroll | With virtual scroll | Improvement |
|
|
170
|
+
|--------|------------------------|---------------------|-------------|
|
|
171
|
+
| Initial render | 750ms | 30ms | **25× faster** |
|
|
172
|
+
| Search keystroke | 200-500ms | 15ms | **13-33× faster** |
|
|
173
|
+
| DOM nodes | 15,000 | ~30 | **99.8% reduction** |
|
|
174
|
+
| Memory usage | ~7.5 MB | ~15 KB | **500× less** |
|
|
175
|
+
|
|
176
|
+
**Configuration:**
|
|
177
|
+
|
|
178
|
+
- `enable-virtual-scroll="true"` — opt-in to virtual scrolling (default: `false`).
|
|
179
|
+
- `virtual-scroll-threshold="100"` — auto-activate when this many items are present (default: `100`).
|
|
180
|
+
- `option-height="50"` — fixed height per option in pixels (default: `50px`).
|
|
181
|
+
- `virtual-scroll-buffer="10"` — extra items rendered above/below viewport for smooth scrolling (default: `10`).
|
|
182
|
+
|
|
183
|
+
**How it works:**
|
|
184
|
+
|
|
185
|
+
- Only renders ~30 visible items instead of all 15,000 DOM elements.
|
|
186
|
+
- Uses absolute positioning with calculated offsets.
|
|
187
|
+
- Maintains 10-item buffer zones above/below viewport for smooth scrolling.
|
|
188
|
+
- Automatically calculates visible range based on scroll position.
|
|
189
|
+
- Works seamlessly with search filtering and selection.
|
|
190
|
+
|
|
191
|
+
**Requirements / limitations:**
|
|
192
|
+
|
|
193
|
+
- All options must have the same fixed height (enforced via CSS).
|
|
194
|
+
- Groups (`<optgroup>`) are disabled in virtual scroll mode — automatically falls back to standard rendering.
|
|
195
|
+
- Works with both filter and navigate search modes.
|
|
196
|
+
|
|
197
|
+
**Example with API data:**
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
<web-multiselect
|
|
201
|
+
id="products"
|
|
202
|
+
enable-virtual-scroll="true"
|
|
203
|
+
search-mode="filter"
|
|
204
|
+
value-member="id"
|
|
205
|
+
display-value-member="name"
|
|
206
|
+
max-height="400px">
|
|
207
|
+
</web-multiselect>
|
|
208
|
+
|
|
209
|
+
<script type="module">
|
|
210
|
+
const select = document.getElementById('products');
|
|
211
|
+
|
|
212
|
+
const response = await fetch('/api/products');
|
|
213
|
+
const products = await response.json();
|
|
214
|
+
|
|
215
|
+
select.options = products; // Could be 10,000+ items
|
|
216
|
+
</script>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Live demo:** [`examples-performance.html`](../examples-performance.html) — working demo with 15,000 randomly generated options.
|
|
220
|
+
|
|
221
|
+
## Search modes: filter vs navigate
|
|
222
|
+
|
|
223
|
+
Choose between two search behaviors:
|
|
224
|
+
|
|
225
|
+
**Filter mode** (default) — hide non-matching options as you type:
|
|
226
|
+
|
|
227
|
+
```html
|
|
228
|
+
<web-multiselect search-mode="filter" id="countries"></web-multiselect>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Navigate mode** — keep all options visible, jump to matches:
|
|
232
|
+
|
|
233
|
+
```html
|
|
234
|
+
<web-multiselect search-mode="navigate" id="states"></web-multiselect>
|
|
235
|
+
|
|
236
|
+
<script>
|
|
237
|
+
const select = document.getElementById('states');
|
|
238
|
+
select.options = [/* ...50 US states... */];
|
|
239
|
+
|
|
240
|
+
// User types "cal" → Jumps to "California", shows all states
|
|
241
|
+
// Matching options are highlighted with left border
|
|
242
|
+
</script>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**When to use each mode:**
|
|
246
|
+
|
|
247
|
+
- **Filter mode** — large datasets where narrowing down is essential (product catalogs, user lists, search results).
|
|
248
|
+
- **Navigate mode** — quick selection from familiar lists (countries, states, keyboard shortcuts, known options).
|
|
249
|
+
|
|
250
|
+
**Key differences:**
|
|
251
|
+
|
|
252
|
+
- Filter mode hides non-matches, navigate mode highlights matches with a left border.
|
|
253
|
+
- Navigate mode keeps previous focus if no match is found (type "xyz" → stays on current option).
|
|
254
|
+
- Navigate mode only works with local data (automatically falls back to filter mode when using `searchCallback`).
|
|
255
|
+
- Both modes respect `beforeSearchCallback` for search term preprocessing (accent removal, validation).
|
|
256
|
+
- **Ctrl+↑/↓** jumps between matches only (navigate mode); regular arrows navigate through all items.
|
|
257
|
+
|
|
258
|
+
## Display modes
|
|
259
|
+
|
|
260
|
+
Perfect for different use cases and space constraints:
|
|
261
|
+
|
|
262
|
+
```html
|
|
263
|
+
<!-- Badges mode (default) - Show all selections as removable badges -->
|
|
264
|
+
<web-multiselect badges-display-mode="pills"></web-multiselect>
|
|
265
|
+
|
|
266
|
+
<!-- Count mode - Show "X selected" text with clear button -->
|
|
267
|
+
<web-multiselect badges-display-mode="count" show-counter="true"></web-multiselect>
|
|
268
|
+
|
|
269
|
+
<!-- Compact mode - Show first item + count in a single removable badge -->
|
|
270
|
+
<web-multiselect badges-display-mode="compact"></web-multiselect>
|
|
271
|
+
<!-- Example output: [JavaScript (+2 more) | x] -->
|
|
272
|
+
|
|
273
|
+
<!-- None mode - No display in badges area (minimal UI) -->
|
|
274
|
+
<web-multiselect badges-display-mode="none" show-counter="true"></web-multiselect>
|
|
275
|
+
<!-- Only shows [X] badge next to toggle icon -->
|
|
276
|
+
|
|
277
|
+
<!-- Auto-switch from badges to count at threshold -->
|
|
278
|
+
<web-multiselect
|
|
279
|
+
badges-threshold="3"
|
|
280
|
+
badges-threshold-mode="count"
|
|
281
|
+
show-counter="true">
|
|
282
|
+
</web-multiselect>
|
|
283
|
+
|
|
284
|
+
<!-- Partial mode - Show limited badges + "+X more" badge -->
|
|
285
|
+
<web-multiselect
|
|
286
|
+
badges-threshold="5"
|
|
287
|
+
badges-threshold-mode="partial"
|
|
288
|
+
badges-max-visible="3">
|
|
289
|
+
</web-multiselect>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Display mode behavior:**
|
|
293
|
+
|
|
294
|
+
- **`pills`** — individual removable badges for each selected item. Calls `getBadgeDisplayCallback` for each item.
|
|
295
|
+
- **`count`** — shows "X selected" text with clear button. Calls `getCounterCallback(count)`.
|
|
296
|
+
- **`compact`** — shows first item + count in single badge (e.g., "JavaScript (+2 more)"). Calls `getBadgeDisplayCallback(firstItem)` and `getCounterCallback(count, remainingCount)`.
|
|
297
|
+
- **`partial`** — shows first N badges + "+X more" badge. Calls `getBadgeDisplayCallback` for visible items and `getCounterCallback(count, remainingCount)` for badge.
|
|
298
|
+
- **`none`** — no display in badges area. No callbacks invoked. Use with `show-counter="true"` for minimal UI.
|
|
299
|
+
|
|
300
|
+
**Badge styling:**
|
|
301
|
+
|
|
302
|
+
- **Data badges** (selected items like "JavaScript", "Python") — blue styling by default.
|
|
303
|
+
- **Badge counters** ("+3 more", "5 selected", compact mode display) — gray styling to distinguish from data.
|
|
304
|
+
- Both can be customized via CSS variables (see `--ms-badge-*` and `--ms-badge-counter-*` in [theming.md](./theming.md)).
|
|
305
|
+
|
|
306
|
+
**Counter (`show-counter="true"`)** — independent feature showing `[X]` next to toggle icon. Works with all display modes. Not affected by callbacks.
|
|
307
|
+
|
|
308
|
+
## Badge positioning
|
|
309
|
+
|
|
310
|
+
Control where selected item badges appear relative to the input:
|
|
311
|
+
|
|
312
|
+
```html
|
|
313
|
+
<!-- Badges below input (default) -->
|
|
314
|
+
<web-multiselect badges-position="bottom"></web-multiselect>
|
|
315
|
+
|
|
316
|
+
<!-- Badges above input -->
|
|
317
|
+
<web-multiselect badges-position="top"></web-multiselect>
|
|
318
|
+
|
|
319
|
+
<!-- Badges to the left of input -->
|
|
320
|
+
<web-multiselect badges-position="left"></web-multiselect>
|
|
321
|
+
|
|
322
|
+
<!-- Badges to the right of input -->
|
|
323
|
+
<web-multiselect badges-position="right"></web-multiselect>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Inline vertical alignment:** For left/right positioning, control vertical alignment with `--ms-inline-align`:
|
|
327
|
+
|
|
328
|
+
```html
|
|
329
|
+
<!-- Center aligned (default) -->
|
|
330
|
+
<web-multiselect badges-position="right" style="--ms-inline-align: center;"></web-multiselect>
|
|
331
|
+
|
|
332
|
+
<!-- Top aligned -->
|
|
333
|
+
<web-multiselect badges-position="right" style="--ms-inline-align: flex-start;"></web-multiselect>
|
|
334
|
+
|
|
335
|
+
<!-- Bottom aligned -->
|
|
336
|
+
<web-multiselect badges-position="left" style="--ms-inline-align: flex-end;"></web-multiselect>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
> **Note:** In RTL mode, left/right positions are automatically mirrored — `badges-position="left"` will appear on the physical right side in RTL languages.
|
|
340
|
+
|
|
341
|
+
## Badge tooltips
|
|
342
|
+
|
|
343
|
+
Enable tooltips on selected item badges with customizable placement and delay:
|
|
344
|
+
|
|
345
|
+
```html
|
|
346
|
+
<!-- Basic tooltips -->
|
|
347
|
+
<web-multiselect
|
|
348
|
+
enable-badge-tooltips="true"
|
|
349
|
+
badge-tooltip-placement="top">
|
|
350
|
+
</web-multiselect>
|
|
351
|
+
|
|
352
|
+
<!-- Fast tooltips with custom delay -->
|
|
353
|
+
<web-multiselect
|
|
354
|
+
enable-badge-tooltips="true"
|
|
355
|
+
badge-tooltip-delay="100">
|
|
356
|
+
</web-multiselect>
|
|
357
|
+
|
|
358
|
+
<script type="module">
|
|
359
|
+
const select = document.querySelector('web-multiselect');
|
|
360
|
+
|
|
361
|
+
// Custom tooltip content
|
|
362
|
+
select.getBadgeTooltipCallback = (item) => {
|
|
363
|
+
return `${item.label} - ${item.subtitle}`;
|
|
364
|
+
};
|
|
365
|
+
</script>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Internationalization (i18n)
|
|
369
|
+
|
|
370
|
+
Customize counter text for proper pluralization and localization:
|
|
371
|
+
|
|
372
|
+
```html
|
|
373
|
+
<web-multiselect
|
|
374
|
+
id="i18n-select"
|
|
375
|
+
badges-threshold="5"
|
|
376
|
+
badges-threshold-mode="partial"
|
|
377
|
+
badges-max-visible="3">
|
|
378
|
+
</web-multiselect>
|
|
379
|
+
|
|
380
|
+
<script type="module">
|
|
381
|
+
const select = document.getElementById('i18n-select');
|
|
382
|
+
|
|
383
|
+
// Spanish pluralization example
|
|
384
|
+
select.getCounterCallback = (count, moreCount) => {
|
|
385
|
+
if (moreCount !== undefined) {
|
|
386
|
+
// Partial mode: "+X more" badge
|
|
387
|
+
return moreCount === 1 ? '+1 más' : `+${moreCount} más`;
|
|
388
|
+
}
|
|
389
|
+
// Count mode: total count
|
|
390
|
+
return count === 1 ? '1 elemento seleccionado' : `${count} elementos seleccionados`;
|
|
391
|
+
};
|
|
392
|
+
</script>
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Right-to-left (RTL) language support
|
|
396
|
+
|
|
397
|
+
Full RTL support for Arabic, Hebrew, Persian, Urdu, and other right-to-left languages with automatic detection and complete UI mirroring:
|
|
398
|
+
|
|
399
|
+
```html
|
|
400
|
+
<!-- Automatic RTL detection from dir attribute -->
|
|
401
|
+
<web-multiselect dir="rtl" search-placeholder="ابحث..."></web-multiselect>
|
|
402
|
+
|
|
403
|
+
<!-- RTL inherited from parent element -->
|
|
404
|
+
<div dir="rtl">
|
|
405
|
+
<web-multiselect search-placeholder="חיפוש..."></web-multiselect>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<!-- RTL on page level -->
|
|
409
|
+
<html dir="rtl">
|
|
410
|
+
<!-- All multi-selects will auto-detect RTL -->
|
|
411
|
+
</html>
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**RTL features:**
|
|
415
|
+
|
|
416
|
+
- **Auto-detection** — detects `dir="rtl"` on component or any ancestor element.
|
|
417
|
+
- **Complete UI mirroring** — toggle icon, text alignment, badges, dropdown.
|
|
418
|
+
- **Logical positioning** — `badges-position="left"` becomes physically right in RTL.
|
|
419
|
+
- **Badge remove buttons** — flip to left side in RTL mode.
|
|
420
|
+
- **Text direction** — all text content properly right-aligned.
|
|
421
|
+
- **No configuration needed** — just set `dir="rtl"` attribute.
|
|
422
|
+
|
|
423
|
+
## Custom rendering
|
|
424
|
+
|
|
425
|
+
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.
|
|
426
|
+
|
|
427
|
+
Three rendering callbacks are available:
|
|
428
|
+
|
|
429
|
+
- **`renderOptionContentCallback`** — customize dropdown option content.
|
|
430
|
+
- **`renderBadgeContentCallback`** — customize badge (selected item) content.
|
|
431
|
+
- **`renderSelectedContentCallback`** — customize selected value text (single-select mode).
|
|
432
|
+
|
|
433
|
+
All callbacks can return either **HTML strings** or **HTMLElement** objects (except `renderSelectedContentCallback`, which returns plain text).
|
|
434
|
+
|
|
435
|
+
### HTML injection (XSS) notice
|
|
436
|
+
|
|
437
|
+
The following callbacks allow **raw HTML injection** and are intentionally **NOT XSS-safe**. This gives developers full control over rendering but requires sanitizing untrusted data:
|
|
438
|
+
|
|
439
|
+
| Callback | Output used in | Risk level |
|
|
440
|
+
|----------|----------------|------------|
|
|
441
|
+
| `renderOptionContentCallback` | Dropdown options (innerHTML) | HTML injection |
|
|
442
|
+
| `renderBadgeContentCallback` | Badges (innerHTML) | HTML injection |
|
|
443
|
+
| `renderSelectedItemContentCallback` | Selected items popover (innerHTML) | HTML injection |
|
|
444
|
+
| `renderGroupLabelContentCallback` | Group headers (innerHTML) | HTML injection |
|
|
445
|
+
| `getIconCallback` | Option icons (innerHTML) | HTML injection |
|
|
446
|
+
| `getSubtitleCallback` | Option subtitles (innerHTML) | HTML injection |
|
|
447
|
+
| `getDisplayValueCallback` | Option titles, badges (innerHTML) | HTML injection |
|
|
448
|
+
| `getBadgeDisplayCallback` | Badge text (innerHTML) | HTML injection |
|
|
449
|
+
| `getCounterCallback` | Count badges (innerHTML) | HTML injection |
|
|
450
|
+
| `getBadgeTooltipCallback` | Tooltips (innerHTML if HTMLElement) | HTML injection |
|
|
451
|
+
| `customStylesCallback` | Style tag (textContent) | CSS injection |
|
|
452
|
+
|
|
453
|
+
**Safe callbacks** (output is escaped or used as data):
|
|
454
|
+
|
|
455
|
+
- `getValueCallback`, `getSearchValueCallback`, `getGroupCallback`, `getDisabledCallback`
|
|
456
|
+
- `getBadgeClassCallback`, `getSelectedItemClassCallback` (CSS class names only)
|
|
457
|
+
- `beforeSearchCallback`, `searchCallback`, `addNewCallback`
|
|
458
|
+
- `selectCallback`, `deselectCallback`, `changeCallback`
|
|
459
|
+
- `getRemoveButtonTooltipCallback` (used as title attribute)
|
|
460
|
+
- `getValueFormatCallback` (form value)
|
|
461
|
+
|
|
462
|
+
**If displaying user-generated content**, sanitize it before returning from these callbacks.
|
|
463
|
+
|
|
464
|
+
### Custom option rendering
|
|
465
|
+
|
|
466
|
+
Customize how options appear in the dropdown:
|
|
467
|
+
|
|
468
|
+
```html
|
|
469
|
+
<web-multiselect id="custom-options"></web-multiselect>
|
|
470
|
+
|
|
471
|
+
<script type="module">
|
|
472
|
+
import '@keenmate/web-multiselect';
|
|
473
|
+
|
|
474
|
+
const select = document.getElementById('custom-options');
|
|
475
|
+
|
|
476
|
+
select.options = [
|
|
477
|
+
{ id: 1, name: 'React', stars: 220000, trending: true },
|
|
478
|
+
{ id: 2, name: 'Vue', stars: 207000, trending: false },
|
|
479
|
+
{ id: 3, name: 'Angular', stars: 94000, trending: false },
|
|
480
|
+
{ id: 4, name: 'Svelte', stars: 76000, trending: true }
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
select.renderOptionContentCallback = (item, context) => {
|
|
484
|
+
// Context provides: { index, isSelected, isFocused, isMatched, isDisabled }
|
|
485
|
+
|
|
486
|
+
return `
|
|
487
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
488
|
+
<strong>${item.name}</strong>
|
|
489
|
+
<span style="color: #666; font-size: 0.875rem;">⭐ ${(item.stars / 1000).toFixed(0)}k</span>
|
|
490
|
+
${item.trending ? '<span style="background: #10b981; color: white; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.75rem;">🔥 Trending</span>' : ''}
|
|
491
|
+
</div>
|
|
492
|
+
`;
|
|
493
|
+
};
|
|
494
|
+
</script>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Context object** (`OptionContentRenderContext`):
|
|
498
|
+
|
|
499
|
+
- `index: number` — index of the option in the filtered list.
|
|
500
|
+
- `isSelected: boolean` — whether the option is currently selected.
|
|
501
|
+
- `isFocused: boolean` — whether the option is currently focused (keyboard navigation).
|
|
502
|
+
- `isMatched: boolean` — whether the option matches the current search term (navigate mode only).
|
|
503
|
+
- `isDisabled: boolean` — whether the option is disabled.
|
|
504
|
+
|
|
505
|
+
### Custom badge rendering
|
|
506
|
+
|
|
507
|
+
Customize how selected items appear as badges:
|
|
508
|
+
|
|
509
|
+
```javascript
|
|
510
|
+
const select = document.querySelector('web-multiselect');
|
|
511
|
+
|
|
512
|
+
select.options = [
|
|
513
|
+
{ id: 1, name: 'John Doe', role: 'Admin', avatar: '👨💼' },
|
|
514
|
+
{ id: 2, name: 'Jane Smith', role: 'Developer', avatar: '👩💻' },
|
|
515
|
+
{ id: 3, name: 'Bob Johnson', role: 'Designer', avatar: '🎨' }
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
// Custom badge rendering in main badges area
|
|
519
|
+
select.renderBadgeContentCallback = (item, context) => {
|
|
520
|
+
return `${item.avatar} ${item.name}`;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Custom rendering for selected items popover (separate callback)
|
|
524
|
+
select.renderSelectedItemContentCallback = (item) => {
|
|
525
|
+
return `
|
|
526
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
527
|
+
<span>${item.avatar}</span>
|
|
528
|
+
<div>
|
|
529
|
+
<div><strong>${item.name}</strong></div>
|
|
530
|
+
<div style="font-size: 0.75rem; color: #666;">${item.role}</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
`;
|
|
534
|
+
};
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Separate callbacks for badges vs. popover:**
|
|
538
|
+
|
|
539
|
+
- `renderBadgeContentCallback` — renders badges in the main badges area (compact display).
|
|
540
|
+
- `renderSelectedItemContentCallback` — renders items in the selected items popover (can be more detailed).
|
|
541
|
+
- If `renderSelectedItemContentCallback` is not defined, falls back to `renderBadgeContentCallback`.
|
|
542
|
+
- Users can assign the same function to both if identical rendering is desired.
|
|
543
|
+
|
|
544
|
+
**Context object** (`BadgeContentRenderContext` for `renderBadgeContentCallback`):
|
|
545
|
+
|
|
546
|
+
- `displayMode: BadgesDisplayMode` — current badges display mode (`'pills'`, `'count'`, `'compact'`, `'partial'`, `'none'`).
|
|
547
|
+
- `isInPopover: boolean` — whether the badge is being rendered in the selected items popover (always false for this callback).
|
|
548
|
+
|
|
549
|
+
### Custom group label rendering
|
|
550
|
+
|
|
551
|
+
Customize how group headers are displayed using `renderGroupLabelContentCallback`:
|
|
552
|
+
|
|
553
|
+
```javascript
|
|
554
|
+
const select = document.querySelector('web-multiselect');
|
|
555
|
+
|
|
556
|
+
select.options = [
|
|
557
|
+
{ value: 'react', label: 'React', group: 'frontend' },
|
|
558
|
+
{ value: 'vue', label: 'Vue', group: 'frontend' },
|
|
559
|
+
{ value: 'nodejs', label: 'Node.js', group: 'backend' },
|
|
560
|
+
{ value: 'postgres', label: 'PostgreSQL', group: 'database' }
|
|
561
|
+
];
|
|
562
|
+
|
|
563
|
+
select.isGroupsAllowed = true;
|
|
564
|
+
select.groupMember = 'group';
|
|
565
|
+
|
|
566
|
+
select.renderGroupLabelContentCallback = (groupName) => {
|
|
567
|
+
const emojis = {
|
|
568
|
+
'frontend': '🎨',
|
|
569
|
+
'backend': '🔧',
|
|
570
|
+
'database': '🗄️'
|
|
571
|
+
};
|
|
572
|
+
const emoji = emojis[groupName] || '📦';
|
|
573
|
+
return `<strong>${emoji} ${groupName.toUpperCase()}</strong>`;
|
|
574
|
+
};
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Signature:** `(groupName: string) => string | HTMLElement`
|
|
578
|
+
|
|
579
|
+
**Use cases:**
|
|
580
|
+
|
|
581
|
+
- Capitalize or format group names.
|
|
582
|
+
- Add icons, emojis, or badges to group headers.
|
|
583
|
+
- Apply HTML formatting (bold, colors, etc.).
|
|
584
|
+
- Internationalization (i18n) — translate group names.
|
|
585
|
+
- Add group-specific metadata or counts.
|
|
586
|
+
|
|
587
|
+
**Notes:**
|
|
588
|
+
|
|
589
|
+
- Keeps standard `.ms__group-label` wrapper for consistent styling.
|
|
590
|
+
- Can return HTML string or HTMLElement.
|
|
591
|
+
- Group name is passed as a string parameter.
|
|
592
|
+
|
|
593
|
+
### Custom badge styling with CSS classes
|
|
594
|
+
|
|
595
|
+
Add custom CSS classes to badges based on item data for semantic styling:
|
|
596
|
+
|
|
597
|
+
```javascript
|
|
598
|
+
const select = document.querySelector('web-multiselect');
|
|
599
|
+
|
|
600
|
+
select.options = [
|
|
601
|
+
{ id: 1, task: 'Fix security bug', priority: 'urgent' },
|
|
602
|
+
{ id: 2, task: 'Update docs', priority: 'normal' },
|
|
603
|
+
{ id: 3, task: 'Refactor code', priority: 'low' }
|
|
604
|
+
];
|
|
605
|
+
|
|
606
|
+
// Add CSS class based on priority
|
|
607
|
+
select.getBadgeClassCallback = (item) => {
|
|
608
|
+
return `badge-${item.priority}`; // Returns 'badge-urgent', 'badge-normal', etc.
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// Can also return array of classes
|
|
612
|
+
select.getBadgeClassCallback = (item) => {
|
|
613
|
+
const classes = [`badge-${item.priority}`];
|
|
614
|
+
if (item.urgent) classes.push('badge-blink');
|
|
615
|
+
return classes;
|
|
616
|
+
};
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
Then style with CSS:
|
|
620
|
+
|
|
621
|
+
```css
|
|
622
|
+
.badge-urgent {
|
|
623
|
+
--ms-badge-text-bg: #fee2e2;
|
|
624
|
+
--ms-badge-text-color: #dc2626;
|
|
625
|
+
--ms-badge-remove-bg: #dc2626;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.badge-normal {
|
|
629
|
+
--ms-badge-text-bg: #dbeafe;
|
|
630
|
+
--ms-badge-text-color: #2563eb;
|
|
631
|
+
--ms-badge-remove-bg: #2563eb;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.badge-low {
|
|
635
|
+
--ms-badge-text-bg: #d1fae5;
|
|
636
|
+
--ms-badge-text-color: #059669;
|
|
637
|
+
--ms-badge-remove-bg: #059669;
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
The callback:
|
|
642
|
+
|
|
643
|
+
- Takes the item as a parameter.
|
|
644
|
+
- Returns a string (single class) or array of strings (multiple classes).
|
|
645
|
+
- Classes are added to the badge's base `.ms__badge` element.
|
|
646
|
+
- Works across all rendering locations (main badges, partial mode, popover).
|
|
647
|
+
|
|
648
|
+
**Separate class callbacks for badges vs. popover:**
|
|
649
|
+
|
|
650
|
+
Similar to rendering callbacks, you can use different class callbacks for badges and selected items:
|
|
651
|
+
|
|
652
|
+
```javascript
|
|
653
|
+
// Add classes to badges in main area
|
|
654
|
+
select.getBadgeClassCallback = (item) => {
|
|
655
|
+
return `badge-${item.priority}`;
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// Add different/additional classes to selected items in popover
|
|
659
|
+
select.getSelectedItemClassCallback = (item) => {
|
|
660
|
+
return [`badge-${item.priority}`, 'badge-detailed'];
|
|
661
|
+
};
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
- `getBadgeClassCallback` — adds classes to badges in the main badges area.
|
|
665
|
+
- `getSelectedItemClassCallback` — adds classes to items in the selected items popover.
|
|
666
|
+
- If `getSelectedItemClassCallback` is not defined, falls back to `getBadgeClassCallback`.
|
|
667
|
+
- Users can assign the same function to both if identical styling is desired.
|
|
668
|
+
|
|
669
|
+
**Shadow DOM CSS injection:**
|
|
670
|
+
|
|
671
|
+
Since the component uses Shadow DOM, regular page CSS cannot style shadow elements. Use `customStylesCallback` to inject CSS directly into the Shadow DOM:
|
|
672
|
+
|
|
673
|
+
```javascript
|
|
674
|
+
const select = document.querySelector('web-multiselect');
|
|
675
|
+
|
|
676
|
+
select.getBadgeClassCallback = (item) => {
|
|
677
|
+
return `badge-${item.priority}`;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
select.customStylesCallback = () => `
|
|
681
|
+
.badge-urgent {
|
|
682
|
+
--ms-badge-text-bg: #fee2e2;
|
|
683
|
+
--ms-badge-text-color: #dc2626;
|
|
684
|
+
--ms-badge-remove-bg: #dc2626;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.badge-normal {
|
|
688
|
+
--ms-badge-text-bg: #dbeafe;
|
|
689
|
+
--ms-badge-text-color: #2563eb;
|
|
690
|
+
--ms-badge-remove-bg: #2563eb;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.badge-low {
|
|
694
|
+
--ms-badge-text-bg: #d1fae5;
|
|
695
|
+
--ms-badge-text-color: #059669;
|
|
696
|
+
--ms-badge-remove-bg: #059669;
|
|
697
|
+
}
|
|
698
|
+
`;
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
The `customStylesCallback`:
|
|
702
|
+
|
|
703
|
+
- Returns a CSS string (not HTML).
|
|
704
|
+
- Styles are injected into the Shadow DOM on initialization.
|
|
705
|
+
- Can be updated dynamically — new styles replace old ones.
|
|
706
|
+
- Works with all custom classes (from `getBadgeClassCallback`, `renderOptionContentCallback`, etc.).
|
|
707
|
+
|
|
708
|
+
### Custom selected item rendering (single-select)
|
|
709
|
+
|
|
710
|
+
Customize the text shown in the input field when in single-select mode:
|
|
711
|
+
|
|
712
|
+
```javascript
|
|
713
|
+
const select = document.querySelector('web-multiselect[multiple="false"]');
|
|
714
|
+
|
|
715
|
+
select.options = [
|
|
716
|
+
{ id: 1, firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
|
|
717
|
+
{ id: 2, firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }
|
|
718
|
+
];
|
|
719
|
+
|
|
720
|
+
// Show just first name when closed
|
|
721
|
+
select.renderSelectedContentCallback = (item) => {
|
|
722
|
+
return item.firstName; // Plain text (not HTML)
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// While dropdown shows full details
|
|
726
|
+
select.getDisplayValueCallback = (item) => {
|
|
727
|
+
return `${item.firstName} ${item.lastName} (${item.email})`;
|
|
728
|
+
};
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### Conditional rendering example
|
|
732
|
+
|
|
733
|
+
Use JavaScript logic for conditional rendering:
|
|
734
|
+
|
|
735
|
+
```javascript
|
|
736
|
+
select.renderOptionContentCallback = (item, context) => {
|
|
737
|
+
const classes = [];
|
|
738
|
+
if (context.isSelected) classes.push('selected');
|
|
739
|
+
if (context.isFocused) classes.push('focused');
|
|
740
|
+
|
|
741
|
+
return `
|
|
742
|
+
<div class="${classes.join(' ')}">
|
|
743
|
+
${item.isNew ? '<span class="badge-new">NEW</span>' : ''}
|
|
744
|
+
<strong>${item.name}</strong>
|
|
745
|
+
${item.description ? `<p style="font-size: 0.875rem; color: #666;">${item.description}</p>` : ''}
|
|
746
|
+
${item.tags ? `<div class="tags">${item.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}</div>` : ''}
|
|
747
|
+
</div>
|
|
748
|
+
`;
|
|
749
|
+
};
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Returning HTMLElement
|
|
753
|
+
|
|
754
|
+
You can also return DOM elements for more complex rendering:
|
|
755
|
+
|
|
756
|
+
```javascript
|
|
757
|
+
select.renderOptionContentCallback = (item, context) => {
|
|
758
|
+
const div = document.createElement('div');
|
|
759
|
+
div.style.display = 'flex';
|
|
760
|
+
div.style.alignItems = 'center';
|
|
761
|
+
div.style.gap = '0.5rem';
|
|
762
|
+
|
|
763
|
+
const img = document.createElement('img');
|
|
764
|
+
img.src = item.avatarUrl;
|
|
765
|
+
img.style.width = '32px';
|
|
766
|
+
img.style.height = '32px';
|
|
767
|
+
img.style.borderRadius = '50%';
|
|
768
|
+
|
|
769
|
+
const span = document.createElement('span');
|
|
770
|
+
span.textContent = item.name;
|
|
771
|
+
|
|
772
|
+
div.appendChild(img);
|
|
773
|
+
div.appendChild(span);
|
|
774
|
+
|
|
775
|
+
return div; // Return HTMLElement instead of string
|
|
776
|
+
};
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Virtual scroll compatibility
|
|
780
|
+
|
|
781
|
+
When using `renderOptionContentCallback` with virtual scroll enabled:
|
|
782
|
+
|
|
783
|
+
> ⚠️ **Important:** Custom option content **must fit within** the configured `optionHeight` (default: 50px).
|
|
784
|
+
|
|
785
|
+
```html
|
|
786
|
+
<web-multiselect
|
|
787
|
+
id="large-dataset"
|
|
788
|
+
enable-virtual-scroll="true"
|
|
789
|
+
option-height="60">
|
|
790
|
+
</web-multiselect>
|
|
791
|
+
|
|
792
|
+
<script type="module">
|
|
793
|
+
const select = document.getElementById('large-dataset');
|
|
794
|
+
|
|
795
|
+
select.renderOptionContentCallback = (item) => {
|
|
796
|
+
return `
|
|
797
|
+
<div style="height: 60px; display: flex; align-items: center;">
|
|
798
|
+
<strong>${item.name}</strong>
|
|
799
|
+
</div>
|
|
800
|
+
`;
|
|
801
|
+
};
|
|
802
|
+
</script>
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
**Virtual scroll requirements:**
|
|
806
|
+
|
|
807
|
+
- Content height must be **fixed** and match `optionHeight`.
|
|
808
|
+
- Overflow will be clipped.
|
|
809
|
+
- Variable-height content only works in non-virtual mode.
|
|
810
|
+
|
|
811
|
+
### Callback priority
|
|
812
|
+
|
|
813
|
+
The component uses a fallback chain when callbacks are not provided:
|
|
814
|
+
|
|
815
|
+
**For options:**
|
|
816
|
+
|
|
817
|
+
1. `renderOptionContentCallback` (full HTML control)
|
|
818
|
+
2. Default: icon + `getDisplayValueCallback` + subtitle
|
|
819
|
+
|
|
820
|
+
**For badges:**
|
|
821
|
+
|
|
822
|
+
1. `renderBadgeContentCallback` (full HTML control)
|
|
823
|
+
2. `getBadgeDisplayCallback` (text only)
|
|
824
|
+
3. `getDisplayValueCallback` (text only)
|
|
825
|
+
|
|
826
|
+
**For selected item (single-select):**
|
|
827
|
+
|
|
828
|
+
1. `renderSelectedContentCallback` (text only)
|
|
829
|
+
2. `getDisplayValueCallback` (text only)
|
|
830
|
+
|
|
831
|
+
### Checkbox control
|
|
832
|
+
|
|
833
|
+
Control checkbox appearance and alignment with CSS variables and attributes:
|
|
834
|
+
|
|
835
|
+
**Checkbox alignment (via attribute):**
|
|
836
|
+
|
|
837
|
+
```html
|
|
838
|
+
<web-multiselect checkbox-align="top"></web-multiselect> <!-- Default -->
|
|
839
|
+
<web-multiselect checkbox-align="center"></web-multiselect> <!-- Middle aligned -->
|
|
840
|
+
<web-multiselect checkbox-align="bottom"></web-multiselect> <!-- Bottom aligned -->
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
**Checkbox size/scale (via CSS):**
|
|
844
|
+
|
|
845
|
+
```html
|
|
846
|
+
<style>
|
|
847
|
+
/* Change checkbox size */
|
|
848
|
+
web-multiselect {
|
|
849
|
+
--ms-checkbox-size: 20px; /* Width and height (default: 16px) */
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/* Scale checkbox */
|
|
853
|
+
web-multiselect {
|
|
854
|
+
--ms-checkbox-scale: 1.5; /* Scale multiplier (default: 1) */
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* Fine-tune checkbox positioning */
|
|
858
|
+
web-multiselect {
|
|
859
|
+
--ms-checkbox-margin-top: 0.5rem; /* Vertical alignment (default: 0.125rem) */
|
|
860
|
+
--ms-checkbox-margin-right: 0; /* Right spacing (default: 0) */
|
|
861
|
+
--ms-checkbox-margin-bottom: 0; /* Bottom spacing (default: 0) */
|
|
862
|
+
--ms-checkbox-margin-left: 0; /* Left spacing (default: 0) */
|
|
863
|
+
}
|
|
864
|
+
</style>
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
**CSS Grid/Flexbox in custom content:**
|
|
868
|
+
|
|
869
|
+
Custom rendering callbacks support full CSS layout control:
|
|
870
|
+
|
|
871
|
+
```javascript
|
|
872
|
+
// CSS Grid example
|
|
873
|
+
multiselect.renderOptionContentCallback = (item, context) => {
|
|
874
|
+
return `
|
|
875
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
|
|
876
|
+
<div><strong>Name:</strong> ${item.name}</div>
|
|
877
|
+
<div><strong>Price:</strong> ${item.price}</div>
|
|
878
|
+
<div><strong>Stock:</strong> ${item.stock}</div>
|
|
879
|
+
<div><strong>Rating:</strong> ${item.rating}</div>
|
|
880
|
+
</div>
|
|
881
|
+
`;
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
// Flexbox example
|
|
885
|
+
multiselect.renderOptionContentCallback = (item, context) => {
|
|
886
|
+
return `
|
|
887
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
888
|
+
<div style="display: flex; flex-direction: column;">
|
|
889
|
+
<strong>${item.name}</strong>
|
|
890
|
+
<span style="font-size: 0.875rem; color: #666;">${item.description}</span>
|
|
891
|
+
</div>
|
|
892
|
+
<div style="text-align: right;">
|
|
893
|
+
<div>${item.price}</div>
|
|
894
|
+
<div style="font-size: 0.875rem;">${item.stock} in stock</div>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
`;
|
|
898
|
+
};
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
**Available CSS variables:**
|
|
902
|
+
|
|
903
|
+
- `--ms-checkbox-size` — checkbox width/height (default: `16px`).
|
|
904
|
+
- `--ms-checkbox-scale` — scale multiplier (default: `1`).
|
|
905
|
+
- `--ms-checkbox-margin-top` — top margin for vertical alignment (default: `0.125rem`).
|
|
906
|
+
- `--ms-checkbox-margin-right` — right margin (default: `0`).
|
|
907
|
+
- `--ms-checkbox-margin-bottom` — bottom margin (default: `0`).
|
|
908
|
+
- `--ms-checkbox-margin-left` — left margin (default: `0`).
|
|
909
|
+
- `--ms-checkbox-align` — alignment value (default: `flex-start`).
|
|
910
|
+
- `--ms-option-gap` — gap between checkbox and content (default: `0.5rem`).
|
|
911
|
+
|
|
912
|
+
> **Note:** Horizontal and bottom margins default to `0` since spacing is handled by flexbox gap. Override for custom layouts.
|
|
913
|
+
|
|
914
|
+
## Flexible data handling
|
|
915
|
+
|
|
916
|
+
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.
|
|
917
|
+
|
|
918
|
+
### Member properties (simple property names)
|
|
919
|
+
|
|
920
|
+
For objects with consistent property names, use member attributes:
|
|
921
|
+
|
|
922
|
+
```html
|
|
923
|
+
<web-multiselect
|
|
924
|
+
id="products"
|
|
925
|
+
value-member="productId"
|
|
926
|
+
display-value-member="productName"
|
|
927
|
+
icon-member="icon"
|
|
928
|
+
subtitle-member="description"
|
|
929
|
+
group-member="category">
|
|
930
|
+
</web-multiselect>
|
|
931
|
+
|
|
932
|
+
<script type="module">
|
|
933
|
+
const select = document.getElementById('products');
|
|
934
|
+
select.options = [
|
|
935
|
+
{
|
|
936
|
+
productId: 'p1',
|
|
937
|
+
productName: 'Laptop',
|
|
938
|
+
icon: '💻',
|
|
939
|
+
description: 'High-performance laptop',
|
|
940
|
+
category: 'Electronics'
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
productId: 'p2',
|
|
944
|
+
productName: 'Mouse',
|
|
945
|
+
icon: '🖱️',
|
|
946
|
+
description: 'Wireless mouse',
|
|
947
|
+
category: 'Electronics'
|
|
948
|
+
}
|
|
949
|
+
];
|
|
950
|
+
</script>
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
### Callback functions (complex logic)
|
|
954
|
+
|
|
955
|
+
For complex data extraction or conditional logic, use callbacks:
|
|
956
|
+
|
|
957
|
+
```javascript
|
|
958
|
+
const select = document.querySelector('web-multiselect');
|
|
959
|
+
|
|
960
|
+
// Custom value extraction
|
|
961
|
+
select.getValueCallback = (item) => item.id || item.code || item.value;
|
|
962
|
+
|
|
963
|
+
// Combine multiple fields for display
|
|
964
|
+
select.getDisplayValueCallback = (item) => {
|
|
965
|
+
return `${item.firstName} ${item.lastName}`;
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// Include multiple fields in search
|
|
969
|
+
select.getSearchValueCallback = (item) => {
|
|
970
|
+
return `${item.name} ${item.sku} ${item.tags.join(' ')}`;
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
// Conditional icons
|
|
974
|
+
select.getIconCallback = (item) => {
|
|
975
|
+
return item.inStock ? '✅' : '❌';
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// Dynamic subtitles
|
|
979
|
+
select.getSubtitleCallback = (item) => {
|
|
980
|
+
return `$${item.price} - ${item.stock} in stock`;
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// Disable based on conditions
|
|
984
|
+
select.getDisabledCallback = (item) => {
|
|
985
|
+
return item.stock === 0 || item.discontinued;
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// Customize badge display (show different text in badges vs dropdown)
|
|
989
|
+
select.getBadgeDisplayCallback = (item) => {
|
|
990
|
+
return item.name;
|
|
991
|
+
};
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
### Tuple array auto-detection
|
|
995
|
+
|
|
996
|
+
The component automatically detects `[key, value]` tuple arrays:
|
|
997
|
+
|
|
998
|
+
```javascript
|
|
999
|
+
select.options = [
|
|
1000
|
+
['js', 'JavaScript'],
|
|
1001
|
+
['ts', 'TypeScript'],
|
|
1002
|
+
['py', 'Python']
|
|
1003
|
+
];
|
|
1004
|
+
// First element becomes value, second becomes display text
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
### Priority order
|
|
1008
|
+
|
|
1009
|
+
When multiple extraction methods are defined, the component uses this priority:
|
|
1010
|
+
|
|
1011
|
+
1. **Callbacks** (highest) — `getValueCallback`, `getDisplayValueCallback`, etc.
|
|
1012
|
+
2. **Member properties** — `valueMember`, `displayValueMember`, etc.
|
|
1013
|
+
3. **Default properties** (lowest) — falls back to `value`, `label`, `name`, etc.
|
|
1014
|
+
|
|
1015
|
+
### TypeScript support
|
|
1016
|
+
|
|
1017
|
+
The component is fully typed with generics:
|
|
1018
|
+
|
|
1019
|
+
```typescript
|
|
1020
|
+
import type { MultiSelectElement } from '@keenmate/web-multiselect';
|
|
1021
|
+
|
|
1022
|
+
interface Product {
|
|
1023
|
+
id: string;
|
|
1024
|
+
name: string;
|
|
1025
|
+
price: number;
|
|
1026
|
+
category: string;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const select = document.querySelector<MultiSelectElement<Product>>('web-multiselect');
|
|
1030
|
+
select.options = [
|
|
1031
|
+
{ id: 'p1', name: 'Laptop', price: 999, category: 'Electronics' }
|
|
1032
|
+
];
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
## Form integration
|
|
1036
|
+
|
|
1037
|
+
The component seamlessly integrates with standard HTML forms by automatically creating hidden inputs in the light DOM (outside Shadow DOM) so FormData can access them.
|
|
1038
|
+
|
|
1039
|
+
### Basic form integration
|
|
1040
|
+
|
|
1041
|
+
```html
|
|
1042
|
+
<form id="userForm" action="/submit" method="POST">
|
|
1043
|
+
<label>Select Skills:</label>
|
|
1044
|
+
<web-multiselect
|
|
1045
|
+
name="skills"
|
|
1046
|
+
value-format="json"
|
|
1047
|
+
multiple="true">
|
|
1048
|
+
</web-multiselect>
|
|
1049
|
+
|
|
1050
|
+
<button type="submit">Submit</button>
|
|
1051
|
+
</form>
|
|
1052
|
+
|
|
1053
|
+
<script type="module">
|
|
1054
|
+
import '@keenmate/web-multiselect';
|
|
1055
|
+
|
|
1056
|
+
const form = document.getElementById('userForm');
|
|
1057
|
+
const select = form.querySelector('web-multiselect');
|
|
1058
|
+
|
|
1059
|
+
select.options = [
|
|
1060
|
+
{ value: 'js', label: 'JavaScript' },
|
|
1061
|
+
{ value: 'ts', label: 'TypeScript' },
|
|
1062
|
+
{ value: 'py', label: 'Python' }
|
|
1063
|
+
];
|
|
1064
|
+
|
|
1065
|
+
form.addEventListener('submit', (e) => {
|
|
1066
|
+
e.preventDefault();
|
|
1067
|
+
const formData = new FormData(form);
|
|
1068
|
+
const skills = formData.get('skills');
|
|
1069
|
+
console.log('Selected skills:', skills);
|
|
1070
|
+
// Output: ["js","ts"] (JSON string)
|
|
1071
|
+
});
|
|
1072
|
+
</script>
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
### Value formats
|
|
1076
|
+
|
|
1077
|
+
Choose how selected values are serialized in forms:
|
|
1078
|
+
|
|
1079
|
+
**JSON format** (default):
|
|
1080
|
+
|
|
1081
|
+
```html
|
|
1082
|
+
<web-multiselect name="items" value-format="json"></web-multiselect>
|
|
1083
|
+
<!-- FormData result: items = ["item1","item2","item3"] -->
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
**CSV format:**
|
|
1087
|
+
|
|
1088
|
+
```html
|
|
1089
|
+
<web-multiselect name="items" value-format="csv"></web-multiselect>
|
|
1090
|
+
<!-- FormData result: items = "item1,item2,item3" -->
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
**Array format** (multiple inputs):
|
|
1094
|
+
|
|
1095
|
+
```html
|
|
1096
|
+
<web-multiselect name="items" value-format="array"></web-multiselect>
|
|
1097
|
+
<!-- FormData result:
|
|
1098
|
+
items[] = "item1"
|
|
1099
|
+
items[] = "item2"
|
|
1100
|
+
items[] = "item3"
|
|
1101
|
+
-->
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
### Custom value formatting
|
|
1105
|
+
|
|
1106
|
+
For advanced use cases, provide a custom formatting function:
|
|
1107
|
+
|
|
1108
|
+
```javascript
|
|
1109
|
+
const select = document.querySelector('web-multiselect');
|
|
1110
|
+
|
|
1111
|
+
select.name = 'product_ids';
|
|
1112
|
+
select.getValueFormatCallback = (values) => {
|
|
1113
|
+
// Custom format: pipe-separated with prefix
|
|
1114
|
+
return values.map(v => `ID:${v}`).join('|');
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// When submitted, FormData will have:
|
|
1118
|
+
// product_ids = "ID:123|ID:456|ID:789"
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### Using getValue() for JavaScript submissions
|
|
1122
|
+
|
|
1123
|
+
For JavaScript-based form submissions (AJAX, fetch), use `getValue()`:
|
|
1124
|
+
|
|
1125
|
+
```javascript
|
|
1126
|
+
// Single-select mode
|
|
1127
|
+
const select = document.querySelector('web-multiselect[multiple="false"]');
|
|
1128
|
+
const selectedId = select.getValue();
|
|
1129
|
+
// Returns: "js" or null
|
|
1130
|
+
|
|
1131
|
+
// Multi-select mode
|
|
1132
|
+
const multiSelect = document.querySelector('web-multiselect[multiple="true"]');
|
|
1133
|
+
const selectedIds = multiSelect.getValue();
|
|
1134
|
+
// Returns: ["js", "ts", "py"] or []
|
|
1135
|
+
|
|
1136
|
+
// Submit with fetch
|
|
1137
|
+
const response = await fetch('/api/update', {
|
|
1138
|
+
method: 'POST',
|
|
1139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1140
|
+
body: JSON.stringify({
|
|
1141
|
+
skills: multiSelect.getValue()
|
|
1142
|
+
})
|
|
1143
|
+
});
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
### Working with numeric values
|
|
1147
|
+
|
|
1148
|
+
The component handles both string and numeric values correctly:
|
|
1149
|
+
|
|
1150
|
+
```javascript
|
|
1151
|
+
select.options = [
|
|
1152
|
+
{ value: 1, label: 'Option 1' },
|
|
1153
|
+
{ value: 2, label: 'Option 2' },
|
|
1154
|
+
{ value: 3, label: 'Option 3' }
|
|
1155
|
+
];
|
|
1156
|
+
|
|
1157
|
+
// getValue() preserves types
|
|
1158
|
+
const values = select.getValue();
|
|
1159
|
+
// Returns: [1, 2, 3] (numbers, not strings)
|
|
1160
|
+
|
|
1161
|
+
// FormData serialization
|
|
1162
|
+
// JSON format: [1,2,3]
|
|
1163
|
+
// CSV format: 1,2,3
|
|
1164
|
+
// Array format: items[]=1, items[]=2, items[]=3
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
## Disabled options
|
|
1168
|
+
|
|
1169
|
+
```javascript
|
|
1170
|
+
select.options = [
|
|
1171
|
+
{ value: 'basic', label: 'Basic License', subtitle: 'Free forever' },
|
|
1172
|
+
{ value: 'pro', label: 'Pro License', subtitle: 'Available for purchase' },
|
|
1173
|
+
{
|
|
1174
|
+
value: 'enterprise',
|
|
1175
|
+
label: 'Enterprise License',
|
|
1176
|
+
subtitle: 'Contact sales',
|
|
1177
|
+
disabled: true
|
|
1178
|
+
}
|
|
1179
|
+
];
|
|
1180
|
+
```
|