@keenmate/web-multiselect 1.12.0-rc01 → 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 +53 -1933
- package/dist/index.d.ts +1 -0
- package/dist/multiselect.js +374 -348
- package/dist/multiselect.umd.js +14 -14
- package/dist/style.css +1 -1
- 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/src/css/base.css +1 -1
- package/src/css/variables.css +5 -6
package/README.md
CHANGED
|
@@ -1,1971 +1,96 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @keenmate/web-multiselect
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://www.npmjs.com/package/@keenmate/web-multiselect)
|
|
5
5
|
|
|
6
|
-
A lightweight,
|
|
6
|
+
> A lightweight, themeable multi-select web component with typeahead search, RTL support, rich content, and full keyboard navigation.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## What is it
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
`@keenmate/web-multiselect` is a custom element (`<web-multiselect>`) that turns a list of options into a searchable, themeable multi-select dropdown. Framework-agnostic — works in React, Vue, Svelte, Blazor, plain HTML.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- **CSS cascade layers (`@layer variables, component, overrides`)** — every internal rule lives in a named layer, so consumer CSS in the light DOM can override any component rule without `!important` or specificity arms races. Document any unlayered rule in your app's CSS and it wins automatically.
|
|
14
|
-
- **CSS file structure refactored into canonical Tier 1+2+3** — `controls.css` for input chrome, `floating.css` for dropdown/hint/tooltip/popover, `states.css` for block-level state modifiers, `badges.css` / `count-display.css` for the two display modes, etc. Each file now owns one logical concern; the old mixed-bag `input-dropdown.css`, `tooltips-popover.css`, `badges-display.css` are gone. Only affects deep CSS imports; bundle output unchanged.
|
|
15
|
-
- **CSS source files no longer have underscore prefix** — `_variables.css` → `variables.css`, etc. The underscore was a leftover SASS partial convention; these are plain CSS modules.
|
|
16
|
-
- **BEM-aligned class names** — `.ms-wrapper` → `.ms__wrapper`, `.ms-debug-info` → `.ms__debug-info`, `.ms-debug-stats` → `.ms__debug-stats`. The wrapper class is internal layout chrome; debug classes are dev-only. See CHANGELOG migration table if you'd styled any of these externally.
|
|
17
|
-
- **Debug panel now fully themeable** — 11 new `--ms-debug-*` variables (was hardcoded hex literals).
|
|
18
|
-
- **`"./manifest"` short-import** — `import manifest from '@keenmate/web-multiselect/manifest'` for tooling that reads the variable catalog.
|
|
19
|
-
- **`test/AUDIT.md`** — living compliance tracker against the BlissFramework web-component guidelines (CSS structure, base variables, color scheme).
|
|
12
|
+
Reads `--base-*` variables from the page if [`@keenmate/theme-designer`](https://theme-designer.keenmate.dev) is present, falls back to sensible OS-aware defaults otherwise, and ships with first-class dark-mode and per-instance theming.
|
|
20
13
|
|
|
21
|
-
|
|
14
|
+
**Headline features:**
|
|
22
15
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
- **New dark-mode e2e suite** — 4 specs verifying WCAG-AA option contrast on a dark page across fully-themed, minimal-override, and pure OS-inheritance configurations.
|
|
16
|
+
- Declarative `<option>` / `<optgroup>` markup — no JavaScript required for simple cases.
|
|
17
|
+
- Virtual scrolling for 10,000+ option datasets (25× faster opening, 99.8% memory reduction).
|
|
18
|
+
- Filter or navigate search modes; async / hybrid search.
|
|
19
|
+
- Five badge display modes (pills, count, compact, partial, none) with positioning on any side.
|
|
20
|
+
- Full keyboard navigation, RTL language support, badge tooltips.
|
|
21
|
+
- Custom rendering callbacks for options, badges, and group headers.
|
|
22
|
+
- Form integration via standard hidden inputs (FormData-compatible).
|
|
31
23
|
|
|
32
|
-
##
|
|
24
|
+
## What's new
|
|
33
25
|
|
|
34
|
-
-
|
|
35
|
-
- ⚡ **Virtual Scrolling** - Handle 15,000+ options instantly (25× faster opening, 99.8% memory reduction)
|
|
36
|
-
- 🔍 **Flexible Search Modes** - Filter (hide non-matches) or navigate (jump to matches, keep all visible)
|
|
37
|
-
- ⌨️ **Keyboard Navigation** - Full keyboard support (arrows, Enter, Esc, Tab)
|
|
38
|
-
- 🎨 **Rich Content** - Icons, subtitles, and multiline text support
|
|
39
|
-
- 📊 **Multiple Display Modes** - Badges, count, compact, partial, or none (minimal UI)
|
|
40
|
-
- 💬 **Badge Tooltips** - Customizable tooltips on selected items with placement control
|
|
41
|
-
- 🎯 **Single & Multi-Select** - Switch between single and multiple selection modes
|
|
42
|
-
- 🔄 **Async Data Loading** - On-demand data fetching support
|
|
43
|
-
- 📦 **Grouped Options** - Organize options into collapsible groups
|
|
44
|
-
- 🎉 **Smart Positioning** - Uses Floating UI for intelligent dropdown placement
|
|
45
|
-
- 🌍 **i18n Support** - Customizable callbacks for pluralization and localization
|
|
46
|
-
- 🌐 **RTL Support** - Full right-to-left language support (Arabic, Hebrew, Persian, Urdu, etc.)
|
|
47
|
-
- ✨ **Modern** - Web Component with Shadow DOM, TypeScript, bundled with Vite
|
|
48
|
-
- 🌐 **Framework Agnostic** - Works with any framework or vanilla JS
|
|
26
|
+
**v1.12.0-rc03** — Two real bug fixes. (1) Declarative `<option>` / `<optgroup>` children now render correctly without needing `value-member` / `display-value-member` / `group-member` attributes — previously every row rendered as `[N/A]`. (2) `select`, `deselect`, and `change` events are now dispatched with `bubbles: true, composed: true`, so Svelte 5 `onchange={fn}`, React `onChange`, Vue `@change`, and any other framework using delegated event routing work without reaching for the legacy `on:` directive. **Behavior change:** delegated form-level `change` listeners will now also receive our event — filter by `e.target.tagName === 'WEB-MULTISELECT'` if your form library extracts state from every `change`. See [CHANGELOG.md](./CHANGELOG.md) for details.
|
|
49
27
|
|
|
50
|
-
|
|
28
|
+
**v1.12.0-rc02** — Badge tooltips now theme correctly when portaled (real dark-mode contrast fix); FOUC-prevention rule renamed to match the actual tag; README split into slim landing page + `docs/` (usage, theming, examples, accessibility). See [CHANGELOG.md](./CHANGELOG.md) for details.
|
|
51
29
|
|
|
52
|
-
|
|
53
|
-
npm install @keenmate/web-multiselect
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Usage
|
|
57
|
-
|
|
58
|
-
### Declarative (No JavaScript!)
|
|
59
|
-
|
|
60
|
-
Perfect for simple forms - just use standard HTML `<option>` elements:
|
|
61
|
-
|
|
62
|
-
```html
|
|
63
|
-
<!-- Simple choice -->
|
|
64
|
-
<web-multiselect multiple="false">
|
|
65
|
-
<option value="yes">Yes</option>
|
|
66
|
-
<option value="no">No</option>
|
|
67
|
-
<option value="maybe" selected>Maybe</option>
|
|
68
|
-
</web-multiselect>
|
|
69
|
-
|
|
70
|
-
<!-- With icons -->
|
|
71
|
-
<web-multiselect>
|
|
72
|
-
<option value="apple" data-icon="🍎">Apple</option>
|
|
73
|
-
<option value="banana" data-icon="🍌" selected>Banana</option>
|
|
74
|
-
<option value="orange" data-icon="🍊">Orange</option>
|
|
75
|
-
</web-multiselect>
|
|
76
|
-
|
|
77
|
-
<!-- With groups -->
|
|
78
|
-
<web-multiselect>
|
|
79
|
-
<optgroup label="Frontend">
|
|
80
|
-
<option value="js" data-icon="🟨">JavaScript</option>
|
|
81
|
-
<option value="ts" data-icon="🔷">TypeScript</option>
|
|
82
|
-
</optgroup>
|
|
83
|
-
<optgroup label="Backend">
|
|
84
|
-
<option value="python" data-icon="🐍" selected>Python</option>
|
|
85
|
-
<option value="java" data-icon="☕">Java</option>
|
|
86
|
-
</optgroup>
|
|
87
|
-
</web-multiselect>
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Programmatic (With JavaScript)
|
|
91
|
-
|
|
92
|
-
For dynamic data and advanced features:
|
|
93
|
-
|
|
94
|
-
```html
|
|
95
|
-
<!-- Multi-select -->
|
|
96
|
-
<web-multiselect
|
|
97
|
-
id="my-select"
|
|
98
|
-
search-placeholder="Search options..."
|
|
99
|
-
initial-values='["js","ts"]'>
|
|
100
|
-
</web-multiselect>
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
// Import the component (includes styles)
|
|
105
|
-
import '@keenmate/web-multiselect';
|
|
106
|
-
|
|
107
|
-
// Or import styles separately if needed
|
|
108
|
-
import '@keenmate/web-multiselect/style.css';
|
|
109
|
-
|
|
110
|
-
const multiselect = document.querySelector('web-multiselect');
|
|
111
|
-
|
|
112
|
-
// Set options programmatically
|
|
113
|
-
multiselect.options = [
|
|
114
|
-
{ value: 'js', label: 'JavaScript', icon: '🟨' },
|
|
115
|
-
{ value: 'ts', label: 'TypeScript', icon: '🔷' },
|
|
116
|
-
{ value: 'py', label: 'Python', icon: '🐍' }
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
// Listen for events
|
|
120
|
-
multiselect.addEventListener('change', (e) => {
|
|
121
|
-
console.log('Selected:', e.detail.selectedOptions);
|
|
122
|
-
console.log('Values:', e.detail.selectedValues);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Public API
|
|
126
|
-
const selected = multiselect.getSelected();
|
|
127
|
-
multiselect.setSelected(['js', 'ts']);
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## Attributes
|
|
131
|
-
|
|
132
|
-
| Attribute | Type | Default | Description |
|
|
133
|
-
|-----------|------|---------|-------------|
|
|
134
|
-
| `multiple` | `boolean` | `true` | Allow multiple selections |
|
|
135
|
-
| `search-placeholder` | `string` | `'Search...'` | Placeholder text for search input |
|
|
136
|
-
| `search-hint` | `string` | - | Hint text shown above input when focused |
|
|
137
|
-
| `allow-groups` | `boolean` | `true` | Enable option grouping |
|
|
138
|
-
| `show-checkboxes` | `boolean` | `true` | Show checkboxes next to options |
|
|
139
|
-
| `close-on-select` | `boolean` | `false` | Close dropdown after selecting |
|
|
140
|
-
| `dropdown-min-width` | `string` | - | Min width for dropdown (e.g., '20rem') |
|
|
141
|
-
| `badges-display-mode` | `'pills' \| 'count' \| 'compact' \| 'partial' \| 'none'` | `'pills'` | How to display selected items. `compact`: first item + count. `none`: no display |
|
|
142
|
-
| `badges-threshold` | `number` | - | Auto-switch mode when exceeded (see badges-threshold-mode) |
|
|
143
|
-
| `badges-threshold-mode` | `'count' \| 'partial'` | `'count'` | Mode after threshold: 'count' shows badge, 'partial' shows limited badges + more badge |
|
|
144
|
-
| `badges-max-visible` | `number` | `3` | Max badges shown in partial mode |
|
|
145
|
-
| `badges-position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | Position of badges container |
|
|
146
|
-
| `show-counter` | `boolean` | `false` | Show [3] badge next to toggle icon |
|
|
147
|
-
| `enable-badge-tooltips` | `boolean` | `false` | Enable tooltips on selected badges |
|
|
148
|
-
| `badge-tooltip-placement` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Tooltip placement relative to badge |
|
|
149
|
-
| `badge-tooltip-delay` | `number` | `100` | Delay in ms before showing tooltip |
|
|
150
|
-
| `badge-tooltip-offset` | `number` | `8` | Distance in pixels between badge and tooltip |
|
|
151
|
-
| `max-height` | `string` | `'20rem'` | Maximum height of dropdown |
|
|
152
|
-
| `empty-message` | `string` | `'No results found'` | Message when no options found |
|
|
153
|
-
| `loading-message` | `string` | `'Loading...'` | Message while loading async data |
|
|
154
|
-
| `min-search-length` | `number` | `0` | Minimum search length for async |
|
|
155
|
-
| `keep-options-on-search` | `boolean` | `true` | Keep initial options visible when searchCallback is active (hybrid search) |
|
|
156
|
-
| `should-keep-search-on-close` | `boolean` | `true` | Preserve search text and filtered results when dropdown closes |
|
|
157
|
-
| `sticky-actions` | `boolean` | `true` | Keep action buttons fixed at top while scrolling |
|
|
158
|
-
| `actions-layout` | `'nowrap' \| 'wrap'` | `'nowrap'` | Layout mode for action buttons: 'nowrap' (single row) or 'wrap' (multi-row) |
|
|
159
|
-
| `lock-placement` | `boolean` | `true` | Lock dropdown placement after first open to prevent flipping |
|
|
160
|
-
| `enable-search` | `boolean` | `true` | Enable/disable search functionality |
|
|
161
|
-
| `search-input-mode` | `'normal' \| 'readonly' \| 'hidden'` | `'normal'` | Search input display mode |
|
|
162
|
-
| `search-mode` | `'filter' \| 'navigate'` | `'filter'` | Search behavior: 'filter' hides non-matches, 'navigate' jumps to matches |
|
|
163
|
-
| `allow-add-new` | `boolean` | `false` | Allow adding new options not in the list |
|
|
164
|
-
| `value-member` | `string` | - | Property name for value/ID extraction from custom objects |
|
|
165
|
-
| `display-value-member` | `string` | - | Property name for display text extraction from custom objects |
|
|
166
|
-
| `search-value-member` | `string` | - | Property name for search text extraction from custom objects |
|
|
167
|
-
| `icon-member` | `string` | - | Property name for icon extraction from custom objects |
|
|
168
|
-
| `subtitle-member` | `string` | - | Property name for subtitle extraction from custom objects |
|
|
169
|
-
| `group-member` | `string` | - | Property name for group extraction from custom objects |
|
|
170
|
-
| `disabled-member` | `string` | - | Property name for disabled state extraction from custom objects |
|
|
171
|
-
| `name` | `string` | - | HTML form field name for form integration (creates hidden input) |
|
|
172
|
-
| `value-format` | `'json' \| 'csv' \| 'array'` | `'json'` | Format for form value serialization |
|
|
173
|
-
| `initial-values` | `string` (JSON array) | - | Pre-selected values |
|
|
174
|
-
| `enable-virtual-scroll` | `boolean` | `false` | Enable virtual scrolling for large datasets |
|
|
175
|
-
| `virtual-scroll-threshold` | `number` | `100` | Minimum items before virtual scroll activates |
|
|
176
|
-
| `option-height` | `number` | `50` | Fixed height for each option in pixels (required for virtual scroll) |
|
|
177
|
-
| `virtual-scroll-buffer` | `number` | `10` | Buffer size - extra items rendered above/below viewport |
|
|
178
|
-
|
|
179
|
-
## Properties
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
// Get/set options
|
|
183
|
-
multiselect.options = [
|
|
184
|
-
{ value: 'js', label: 'JavaScript' },
|
|
185
|
-
{ value: 'ts', label: 'TypeScript' }
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
// Async data loading
|
|
189
|
-
multiselect.onSearch = async (searchTerm) => {
|
|
190
|
-
const response = await fetch(`/api/search?q=${searchTerm}`);
|
|
191
|
-
return await response.json();
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
// Pre-process search terms before calling searchCallback
|
|
195
|
-
multiselect.beforeSearchCallback = (searchTerm) => {
|
|
196
|
-
// Remove accents: "café" → "cafe"
|
|
197
|
-
const normalized = searchTerm.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
198
|
-
|
|
199
|
-
// Block search if too short (return null to prevent search)
|
|
200
|
-
if (normalized.length < 2) return null;
|
|
201
|
-
|
|
202
|
-
return normalized; // Return transformed term
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// Event callbacks
|
|
206
|
-
multiselect.onSelect = (option) => {
|
|
207
|
-
console.log('Selected:', option);
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
multiselect.onDeselect = (option) => {
|
|
211
|
-
console.log('Deselected:', option);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
multiselect.onChange = (selectedOptions) => {
|
|
215
|
-
console.log('Changed:', selectedOptions);
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
// Badge display customization (show different text in badges vs dropdown)
|
|
219
|
-
multiselect.getBadgeDisplayCallback = (item) => {
|
|
220
|
-
// Show shorter text in badges (e.g., just name instead of "name (email)")
|
|
221
|
-
return item.name; // Dropdown might show "John Doe (john@example.com)"
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
// Badge tooltip customization
|
|
225
|
-
multiselect.getBadgeTooltipCallback = (item) => {
|
|
226
|
-
return `${item.label} - ${item.subtitle}`;
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
// Action buttons (Select All, Clear All, custom actions)
|
|
230
|
-
multiselect.actionButtons = [
|
|
231
|
-
{
|
|
232
|
-
action: 'select-all',
|
|
233
|
-
text: 'Select All',
|
|
234
|
-
tooltip: 'Select all items',
|
|
235
|
-
cssClass: 'my-custom-class',
|
|
236
|
-
isVisibleCallback: (multiselect) => multiselect.getSelected().length < 5 // Hide if 5+ selected
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
action: 'clear-all',
|
|
240
|
-
text: 'Clear All',
|
|
241
|
-
tooltip: 'Clear selection',
|
|
242
|
-
isVisible: true, // Static visibility
|
|
243
|
-
isDisabled: false // Static disabled state
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
action: 'custom',
|
|
247
|
-
text: 'Invert',
|
|
248
|
-
tooltip: 'Invert selection',
|
|
249
|
-
onClick: (multiselect) => {
|
|
250
|
-
// Custom action - invert selection
|
|
251
|
-
const allValues = multiselect.options.map(opt => opt.value);
|
|
252
|
-
const selectedValues = multiselect.getValue();
|
|
253
|
-
const inverted = allValues.filter(v => !selectedValues.includes(v));
|
|
254
|
-
multiselect.setSelected(inverted);
|
|
255
|
-
},
|
|
256
|
-
// Dynamic callbacks (take priority over static properties)
|
|
257
|
-
isDisabledCallback: (multiselect) => multiselect.getSelected().length === 0,
|
|
258
|
-
getTextCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'Invert' : 'Select Items First',
|
|
259
|
-
getClassCallback: (multiselect) => multiselect.getSelected().length > 0 ? 'active' : 'inactive'
|
|
260
|
-
}
|
|
261
|
-
];
|
|
262
|
-
|
|
263
|
-
// Counter i18n/pluralization
|
|
264
|
-
multiselect.getCounterCallback = (count, moreCount) => {
|
|
265
|
-
if (moreCount !== undefined) {
|
|
266
|
-
return `+${moreCount} more`; // Partial mode badge
|
|
267
|
-
}
|
|
268
|
-
return `${count} selected`; // Count mode display
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
// Data extraction - Member properties (for simple property names)
|
|
272
|
-
multiselect.valueMember = 'id';
|
|
273
|
-
multiselect.displayValueMember = 'name';
|
|
274
|
-
multiselect.iconMember = 'icon';
|
|
275
|
-
multiselect.subtitleMember = 'description';
|
|
276
|
-
multiselect.groupMember = 'category';
|
|
277
|
-
multiselect.disabledMember = 'isDisabled';
|
|
278
|
-
|
|
279
|
-
// Data extraction - Callback functions (for complex logic)
|
|
280
|
-
multiselect.getValueCallback = (item) => item.id || item.value;
|
|
281
|
-
multiselect.getDisplayValueCallback = (item) => item.label || item.name;
|
|
282
|
-
multiselect.getSearchValueCallback = (item) => `${item.name} ${item.tags.join(' ')}`;
|
|
283
|
-
multiselect.getIconCallback = (item) => item.icon || '📄';
|
|
284
|
-
multiselect.getSubtitleCallback = (item) => `${item.price} - ${item.stock} in stock`;
|
|
285
|
-
multiselect.getGroupCallback = (item) => item.category;
|
|
286
|
-
multiselect.getDisabledCallback = (item) => item.stock === 0;
|
|
287
|
-
|
|
288
|
-
// Custom rendering - Full HTML control
|
|
289
|
-
multiselect.renderGroupLabelContentCallback = (groupName) => {
|
|
290
|
-
// Customize group header display (HTML string or HTMLElement)
|
|
291
|
-
return `<strong>📦 ${groupName.toUpperCase()}</strong>`;
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
multiselect.renderOptionContentCallback = (item, context) => {
|
|
295
|
-
// Customize option content (HTML string or HTMLElement)
|
|
296
|
-
return `<strong>${item.name}</strong> <span class="badge">${item.status}</span>`;
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
multiselect.renderBadgeContentCallback = (item, context) => {
|
|
300
|
-
// Customize badge content (HTML string or HTMLElement)
|
|
301
|
-
return context.isInPopover
|
|
302
|
-
? `${item.icon} ${item.name} - ${item.description}`
|
|
303
|
-
: `${item.icon} ${item.name}`;
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
multiselect.renderSelectedContentCallback = (item) => {
|
|
307
|
-
// Customize selected item text in single-select mode (plain text only)
|
|
308
|
-
return item.firstName; // Show just first name when closed
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
// Form integration
|
|
312
|
-
multiselect.name = 'selected_items';
|
|
313
|
-
multiselect.valueFormat = 'json'; // 'json' | 'csv' | 'array'
|
|
314
|
-
multiselect.getValueFormatCallback = (values) => values.join('|'); // Custom format
|
|
315
|
-
|
|
316
|
-
// Read-only properties
|
|
317
|
-
const selectedValue = multiselect.selectedValue; // string | number | array | null
|
|
318
|
-
const selectedItem = multiselect.selectedItem; // First selected item object
|
|
319
|
-
|
|
320
|
-
// Add new option callback
|
|
321
|
-
multiselect.addNewCallback = async (value) => {
|
|
322
|
-
// Validate and create new option
|
|
323
|
-
const newOption = await fetch('/api/options', {
|
|
324
|
-
method: 'POST',
|
|
325
|
-
body: JSON.stringify({ name: value })
|
|
326
|
-
}).then(r => r.json());
|
|
327
|
-
return newOption;
|
|
328
|
-
};
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
## Methods
|
|
332
|
-
|
|
333
|
-
| Method | Description |
|
|
334
|
-
|--------|-------------|
|
|
335
|
-
| `getSelected()` | Get currently selected options as array of option objects |
|
|
336
|
-
| `setSelected(values: (string \| number)[])` | Set selected values by ID/value |
|
|
337
|
-
| `getValue()` | Get selected value(s) - returns single value in single-select mode, array in multi-select mode |
|
|
338
|
-
| `destroy()` | Clean up and destroy instance |
|
|
339
|
-
|
|
340
|
-
## Events
|
|
341
|
-
|
|
342
|
-
| Event | Detail | Description |
|
|
343
|
-
|-------|--------|-------------|
|
|
344
|
-
| `select` | `{ option, selectedOptions }` | Fired when an option is selected |
|
|
345
|
-
| `deselect` | `{ option, selectedOptions }` | Fired when an option is deselected |
|
|
346
|
-
| `change` | `{ selectedOptions, selectedValues }` | Fired when selection changes |
|
|
347
|
-
|
|
348
|
-
## Keyboard Shortcuts
|
|
349
|
-
|
|
350
|
-
- **↑ ↓** - Navigate up/down through options
|
|
351
|
-
- **Ctrl+↑ Ctrl+↓** - Jump between matched items (navigate mode only)
|
|
352
|
-
- **Page Up / Page Down** - Move focus by 10 options at a time
|
|
353
|
-
- **Home / End** - Jump to first / last option
|
|
354
|
-
- **Enter** - Select focused option (or add new entry when `allow-add-new="true"` and the search has text)
|
|
355
|
-
- **Escape** - Close popover → Clear search → Close dropdown (priority order)
|
|
356
|
-
- **Tab** - Close dropdown and move to next field
|
|
357
|
-
- **Type** - Filter options by search term
|
|
358
|
-
|
|
359
|
-
> 💡 To surface these shortcuts to your users, set the `search-hint` attribute — the hint floats above the input when focused. Example: `<web-multiselect search-mode="navigate" search-hint="Ctrl/Cmd + ↓ / ↑ to jump between matches">`.
|
|
360
|
-
|
|
361
|
-
## Advanced Features
|
|
362
|
-
|
|
363
|
-
### Rich Content with Icons
|
|
364
|
-
|
|
365
|
-
Icons support multiple formats - emojis, SVG markup, Font Awesome, images, or any HTML:
|
|
366
|
-
|
|
367
|
-
```html
|
|
368
|
-
<web-multiselect id="frameworks"></web-multiselect>
|
|
369
|
-
|
|
370
|
-
<script type="module">
|
|
371
|
-
const select = document.getElementById('frameworks');
|
|
372
|
-
select.options = [
|
|
373
|
-
{
|
|
374
|
-
value: 'react',
|
|
375
|
-
label: 'React',
|
|
376
|
-
icon: '⚛️', // Emoji
|
|
377
|
-
subtitle: 'A JavaScript library for building user interfaces'
|
|
378
|
-
},
|
|
379
|
-
{
|
|
380
|
-
value: 'vue',
|
|
381
|
-
label: 'Vue.js',
|
|
382
|
-
icon: '<svg viewBox="0 0 24 24"><path d="M2 3l10 18L22 3h-4l-6 10.5L6 3H2z"/></svg>', // SVG
|
|
383
|
-
subtitle: 'The Progressive JavaScript Framework'
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
value: 'angular',
|
|
387
|
-
label: 'Angular',
|
|
388
|
-
icon: '<i class="fab fa-angular"></i>', // Font Awesome
|
|
389
|
-
subtitle: 'Platform for building mobile and desktop apps'
|
|
390
|
-
},
|
|
391
|
-
{
|
|
392
|
-
value: 'svelte',
|
|
393
|
-
label: 'Svelte',
|
|
394
|
-
icon: '<img src="svelte-logo.png" alt="Svelte" />', // Image
|
|
395
|
-
subtitle: 'Cybernetically enhanced web apps'
|
|
396
|
-
}
|
|
397
|
-
];
|
|
398
|
-
</script>
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
### Grouped Options
|
|
402
|
-
|
|
403
|
-
```javascript
|
|
404
|
-
select.options = [
|
|
405
|
-
{ value: 'js', label: 'JavaScript', group: 'Frontend' },
|
|
406
|
-
{ value: 'ts', label: 'TypeScript', group: 'Frontend' },
|
|
407
|
-
{ value: 'python', label: 'Python', group: 'Backend' },
|
|
408
|
-
{ value: 'java', label: 'Java', group: 'Backend' }
|
|
409
|
-
];
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### Async Data Loading
|
|
413
|
-
|
|
414
|
-
```html
|
|
415
|
-
<web-multiselect
|
|
416
|
-
id="async-select"
|
|
417
|
-
min-search-length="2"
|
|
418
|
-
loading-message="Searching..."
|
|
419
|
-
empty-message="No products found">
|
|
420
|
-
</web-multiselect>
|
|
421
|
-
|
|
422
|
-
<script type="module">
|
|
423
|
-
const select = document.getElementById('async-select');
|
|
424
|
-
|
|
425
|
-
select.onSearch = async (searchTerm) => {
|
|
426
|
-
const response = await fetch(`/api/products?q=${searchTerm}`);
|
|
427
|
-
const data = await response.json();
|
|
428
|
-
return data.products;
|
|
429
|
-
};
|
|
430
|
-
</script>
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
### Hybrid Static + Dynamic Search
|
|
434
|
-
|
|
435
|
-
Show popular items initially, then switch to full database search when the user types. Perfect for showing "Top 10" items while supporting comprehensive search:
|
|
436
|
-
|
|
437
|
-
```html
|
|
438
|
-
<web-multiselect
|
|
439
|
-
id="hybrid-select"
|
|
440
|
-
min-search-length="3"
|
|
441
|
-
keep-options-on-search="true">
|
|
442
|
-
</web-multiselect>
|
|
443
|
-
|
|
444
|
-
<script type="module">
|
|
445
|
-
const select = document.getElementById('hybrid-select');
|
|
446
|
-
|
|
447
|
-
// Set initial popular items (shown when dropdown opens)
|
|
448
|
-
select.options = [
|
|
449
|
-
{ id: 1, name: 'React' },
|
|
450
|
-
{ id: 2, name: 'Vue' },
|
|
451
|
-
{ id: 3, name: 'Angular' },
|
|
452
|
-
{ id: 4, name: 'Svelte' },
|
|
453
|
-
{ id: 5, name: 'Solid' }
|
|
454
|
-
];
|
|
455
|
-
|
|
456
|
-
// Pre-process search terms (remove accents, validate, etc.)
|
|
457
|
-
select.beforeSearchCallback = (searchTerm) => {
|
|
458
|
-
// Remove accents: "café" → "cafe"
|
|
459
|
-
const normalized = searchTerm.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
460
|
-
|
|
461
|
-
// Block search if too short (return null to prevent search)
|
|
462
|
-
if (normalized.length < 2) return null;
|
|
463
|
-
|
|
464
|
-
return normalized;
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
// Search full database when user types 3+ characters
|
|
468
|
-
select.onSearch = async (searchTerm) => {
|
|
469
|
-
const response = await fetch(`/api/frameworks/search?q=${searchTerm}`);
|
|
470
|
-
return await response.json();
|
|
471
|
-
};
|
|
472
|
-
</script>
|
|
473
|
-
```
|
|
474
|
-
|
|
475
|
-
**How it works:**
|
|
476
|
-
1. **Dropdown opens** → Shows 5 popular frameworks
|
|
477
|
-
2. **User types "rea"** → Calls API, shows all matching results from database
|
|
478
|
-
3. **User clears search** → Shows 5 popular frameworks again
|
|
479
|
-
4. **User types "café"** → `beforeSearchCallback` converts to "cafe", then searches
|
|
480
|
-
|
|
481
|
-
**Key options:**
|
|
482
|
-
- `keep-options-on-search="true"` (default) - Keep initial options visible when search is empty/short
|
|
483
|
-
- `beforeSearchCallback` - Transform search text or block search by returning `null`
|
|
484
|
-
- `min-search-length` - Minimum characters before triggering search (shows initial options below this)
|
|
485
|
-
|
|
486
|
-
### Virtual Scrolling for Large Datasets
|
|
487
|
-
|
|
488
|
-
Handle 10,000+ options with smooth 60fps performance by rendering only visible items:
|
|
489
|
-
|
|
490
|
-
```html
|
|
491
|
-
<web-multiselect
|
|
492
|
-
id="large-dataset"
|
|
493
|
-
enable-virtual-scroll="true"
|
|
494
|
-
virtual-scroll-threshold="100"
|
|
495
|
-
option-height="50"
|
|
496
|
-
virtual-scroll-buffer="10"
|
|
497
|
-
search-mode="filter"
|
|
498
|
-
max-height="400px">
|
|
499
|
-
</web-multiselect>
|
|
500
|
-
|
|
501
|
-
<script type="module">
|
|
502
|
-
import '@keenmate/web-multiselect';
|
|
503
|
-
|
|
504
|
-
const select = document.getElementById('large-dataset');
|
|
505
|
-
|
|
506
|
-
// Generate 15,000 options
|
|
507
|
-
const largeDataset = Array.from({ length: 15000 }, (_, i) => ({
|
|
508
|
-
value: i,
|
|
509
|
-
label: `Item ${i.toString().padStart(5, '0')}`
|
|
510
|
-
}));
|
|
511
|
-
|
|
512
|
-
select.options = largeDataset;
|
|
513
|
-
</script>
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
**Performance Comparison (15,000 items):**
|
|
517
|
-
|
|
518
|
-
| Metric | Without Virtual Scroll | With Virtual Scroll | Improvement |
|
|
519
|
-
|--------|------------------------|---------------------|-------------|
|
|
520
|
-
| Initial render | 750ms | 30ms | **25× faster** |
|
|
521
|
-
| Search keystroke | 200-500ms | 15ms | **13-33× faster** |
|
|
522
|
-
| DOM nodes | 15,000 | ~30 | **99.8% reduction** |
|
|
523
|
-
| Memory usage | ~7.5 MB | ~15 KB | **500× less** |
|
|
524
|
-
|
|
525
|
-
**Configuration:**
|
|
526
|
-
|
|
527
|
-
- `enable-virtual-scroll="true"` - Enable virtual scrolling (default: `false`)
|
|
528
|
-
- `virtual-scroll-threshold="100"` - Auto-activate when this many items are present (default: `100`)
|
|
529
|
-
- `option-height="50"` - Fixed height per option in pixels (default: `50px`)
|
|
530
|
-
- `virtual-scroll-buffer="10"` - Extra items rendered above/below viewport for smooth scrolling (default: `10`)
|
|
531
|
-
|
|
532
|
-
**How it works:**
|
|
533
|
-
- Only renders ~30 visible items instead of all 15,000 DOM elements
|
|
534
|
-
- Uses absolute positioning with calculated offsets
|
|
535
|
-
- Maintains 10-item buffer zones above/below viewport for smooth scrolling
|
|
536
|
-
- Automatically calculates visible range based on scroll position
|
|
537
|
-
- Works seamlessly with search filtering and selection
|
|
538
|
-
|
|
539
|
-
**Requirements:**
|
|
540
|
-
- All options must have the same fixed height (enforced via CSS)
|
|
541
|
-
- Not compatible with grouped options (automatically falls back to normal rendering)
|
|
542
|
-
- Works with both filter and navigate search modes
|
|
543
|
-
|
|
544
|
-
**Example with search:**
|
|
545
|
-
```html
|
|
546
|
-
<!-- Virtual scroll + filter search for optimal large dataset performance -->
|
|
547
|
-
<web-multiselect
|
|
548
|
-
id="products"
|
|
549
|
-
enable-virtual-scroll="true"
|
|
550
|
-
search-mode="filter"
|
|
551
|
-
value-member="id"
|
|
552
|
-
display-value-member="name"
|
|
553
|
-
max-height="400px">
|
|
554
|
-
</web-multiselect>
|
|
555
|
-
|
|
556
|
-
<script type="module">
|
|
557
|
-
const select = document.getElementById('products');
|
|
558
|
-
|
|
559
|
-
// Load from API
|
|
560
|
-
const response = await fetch('/api/products');
|
|
561
|
-
const products = await response.json();
|
|
562
|
-
|
|
563
|
-
select.options = products; // Could be 10,000+ items
|
|
564
|
-
</script>
|
|
565
|
-
```
|
|
30
|
+
> ⚠️ **Security notice:** This component intentionally allows raw HTML in rendering callbacks to give developers full control over content display. If you display user-generated content, you must sanitize it yourself. See [docs/examples.md → HTML Injection (XSS) notice](./docs/examples.md#html-injection-xss-notice) for the complete list of affected callbacks.
|
|
566
31
|
|
|
567
|
-
|
|
568
|
-
See [examples-performance.html](examples-performance.html) for a working demo with 15,000 randomly generated options.
|
|
32
|
+
## Demos & docs
|
|
569
33
|
|
|
570
|
-
|
|
34
|
+
- 🚀 [Live demo](https://web-multiselect.keenmate.dev)
|
|
35
|
+
- 📘 [Usage / API reference](./docs/usage.md) — attributes, properties, methods, events.
|
|
36
|
+
- 🎨 [Theming](./docs/theming.md) — `--ms-*` variables, dark mode, cascade layers, Theme Designer integration.
|
|
37
|
+
- 📚 [Examples / cookbook](./docs/examples.md) — rich content, async search, virtual scroll, custom rendering, forms.
|
|
38
|
+
- ♿ [Accessibility](./docs/accessibility.md) — keyboard model, ARIA labels, focus behavior.
|
|
571
39
|
|
|
572
|
-
|
|
40
|
+
## Install
|
|
573
41
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
<web-multiselect
|
|
577
|
-
enable-virtual-scroll="true"
|
|
578
|
-
virtual-scroll-threshold="100"
|
|
579
|
-
option-height="50"
|
|
580
|
-
virtual-scroll-buffer="10">
|
|
581
|
-
</web-multiselect>
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
**Performance improvements with 15,000 items:**
|
|
585
|
-
- **Dropdown opening**: 750ms → 30ms (25× faster)
|
|
586
|
-
- **Search performance**: 200-500ms → 15ms per keystroke (13-33× faster)
|
|
587
|
-
- **Memory usage**: 7.5 MB → 15 KB (99.8% reduction)
|
|
588
|
-
- **DOM nodes**: 15,000 → ~30 visible items
|
|
589
|
-
|
|
590
|
-
**Configuration:**
|
|
591
|
-
- `enable-virtual-scroll="true"` - Opt-in to virtual scrolling
|
|
592
|
-
- `virtual-scroll-threshold="100"` - Auto-activates at 100+ items (default)
|
|
593
|
-
- `option-height="50"` - Fixed height per option in pixels (default: 50px)
|
|
594
|
-
- `virtual-scroll-buffer="10"` - Extra items rendered above/below viewport (default: 10)
|
|
595
|
-
|
|
596
|
-
**Features:**
|
|
597
|
-
- Full keyboard navigation (arrows, Page Up/Down, Home/End)
|
|
598
|
-
- Smooth mouse wheel scrolling
|
|
599
|
-
- Drag scrollbar support
|
|
600
|
-
- Works with search in both filter and navigate modes
|
|
601
|
-
- Automatic activation based on threshold
|
|
602
|
-
|
|
603
|
-
**Limitations:**
|
|
604
|
-
- Groups (`<optgroup>`) are disabled in virtual scroll mode (automatically falls back to standard rendering)
|
|
605
|
-
- All options must have consistent height (enforced via CSS)
|
|
606
|
-
|
|
607
|
-
**Live Demo:**
|
|
608
|
-
See [examples-performance.html](examples-performance.html) for a working demo testing virtual scroll with 15,000 randomly generated options.
|
|
609
|
-
|
|
610
|
-
### Search Modes: Filter vs Navigate
|
|
611
|
-
|
|
612
|
-
Choose between two search behaviors:
|
|
613
|
-
|
|
614
|
-
**Filter Mode** (default) - Hide non-matching options as you type:
|
|
615
|
-
```html
|
|
616
|
-
<web-multiselect search-mode="filter" id="countries"></web-multiselect>
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
**Navigate Mode** - Keep all options visible, jump to matches:
|
|
620
|
-
```html
|
|
621
|
-
<web-multiselect search-mode="navigate" id="states"></web-multiselect>
|
|
622
|
-
|
|
623
|
-
<script>
|
|
624
|
-
const select = document.getElementById('states');
|
|
625
|
-
select.options = [...50 US states...];
|
|
626
|
-
|
|
627
|
-
// User types "cal" → Jumps to "California", shows all states
|
|
628
|
-
// Matching options are highlighted with left border
|
|
629
|
-
</script>
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
**When to use each mode:**
|
|
633
|
-
- **Filter Mode**: Large datasets where narrowing down is essential (product catalogs, user lists, search results)
|
|
634
|
-
- **Navigate Mode**: Quick selection from familiar lists (countries, states, keyboard shortcuts, known options)
|
|
635
|
-
|
|
636
|
-
**Key differences:**
|
|
637
|
-
- Filter mode hides non-matches, navigate mode highlights matches with a left border
|
|
638
|
-
- Navigate mode keeps previous focus if no match is found (type "xyz" → stays on current option)
|
|
639
|
-
- Navigate mode only works with local data (automatically falls back to filter mode when using `searchCallback`)
|
|
640
|
-
- Both modes respect `beforeSearchCallback` for search term preprocessing (accent removal, validation)
|
|
641
|
-
- **Ctrl+↑/↓** jumps between matches only (navigate mode) - regular arrows navigate through all items
|
|
642
|
-
|
|
643
|
-
### Display Modes
|
|
644
|
-
|
|
645
|
-
Perfect for different use cases and space constraints:
|
|
646
|
-
|
|
647
|
-
```html
|
|
648
|
-
<!-- Badges mode (default) - Show all selections as removable badges -->
|
|
649
|
-
<web-multiselect badges-display-mode="pills"></web-multiselect>
|
|
650
|
-
|
|
651
|
-
<!-- Count mode - Show "X selected" text with clear button -->
|
|
652
|
-
<web-multiselect badges-display-mode="count" show-counter="true"></web-multiselect>
|
|
653
|
-
|
|
654
|
-
<!-- Compact mode - Show first item + count in a single removable badge -->
|
|
655
|
-
<web-multiselect badges-display-mode="compact"></web-multiselect>
|
|
656
|
-
<!-- Example output: [JavaScript (+2 more) | x] -->
|
|
657
|
-
|
|
658
|
-
<!-- None mode - No display in badges area (minimal UI) -->
|
|
659
|
-
<web-multiselect badges-display-mode="none" show-counter="true"></web-multiselect>
|
|
660
|
-
<!-- Only shows [X] badge next to toggle icon -->
|
|
661
|
-
|
|
662
|
-
<!-- Auto-switch from badges to count at threshold -->
|
|
663
|
-
<web-multiselect
|
|
664
|
-
badges-threshold="3"
|
|
665
|
-
badges-threshold-mode="count"
|
|
666
|
-
show-counter="true">
|
|
667
|
-
</web-multiselect>
|
|
668
|
-
|
|
669
|
-
<!-- Partial mode - Show limited badges + "+X more" badge -->
|
|
670
|
-
<web-multiselect
|
|
671
|
-
badges-threshold="5"
|
|
672
|
-
badges-threshold-mode="partial"
|
|
673
|
-
badges-max-visible="3">
|
|
674
|
-
</web-multiselect>
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
**Display Mode Behavior:**
|
|
678
|
-
- **`pills`**: Individual removable badges for each selected item. Calls `getBadgeDisplayCallback` for each item.
|
|
679
|
-
- **`count`**: Shows "X selected" text with clear button. Calls `getCounterCallback(count)`.
|
|
680
|
-
- **`compact`**: Shows first item + count in single badge (e.g., "JavaScript (+2 more)"). Calls `getBadgeDisplayCallback(firstItem)` and `getCounterCallback(count, remainingCount)`.
|
|
681
|
-
- **`partial`**: Shows first N badges + "+X more" badge. Calls `getBadgeDisplayCallback` for visible items and `getCounterCallback(count, remainingCount)` for badge.
|
|
682
|
-
- **`none`**: No display in badges area. No callbacks invoked. Use with `show-counter="true"` for minimal UI.
|
|
683
|
-
|
|
684
|
-
**Badge Styling:**
|
|
685
|
-
- **Data badges** (selected items like "JavaScript", "Python"): Blue styling by default
|
|
686
|
-
- **BadgeCounters** ("+3 more", "5 selected", compact mode display): Gray styling to distinguish from data
|
|
687
|
-
- Both can be customized via CSS variables (see `--ms-badge-*` and `--ms-badge-counter-*`)
|
|
688
|
-
|
|
689
|
-
**Counter (`show-counter="true"`)**: Independent feature showing `[X]` next to toggle icon. Works with all display modes. Not affected by callbacks.
|
|
690
|
-
|
|
691
|
-
### Badge Positioning
|
|
692
|
-
|
|
693
|
-
Control where selected item badges appear relative to the input:
|
|
694
|
-
|
|
695
|
-
```html
|
|
696
|
-
<!-- Badges below input (default) -->
|
|
697
|
-
<web-multiselect badges-position="bottom"></web-multiselect>
|
|
698
|
-
|
|
699
|
-
<!-- Badges above input -->
|
|
700
|
-
<web-multiselect badges-position="top"></web-multiselect>
|
|
701
|
-
|
|
702
|
-
<!-- Badges to the left of input -->
|
|
703
|
-
<web-multiselect badges-position="left"></web-multiselect>
|
|
704
|
-
|
|
705
|
-
<!-- Badges to the right of input -->
|
|
706
|
-
<web-multiselect badges-position="right"></web-multiselect>
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
**Inline Vertical Alignment:** For left/right positioning, control vertical alignment with `--ms-inline-align`:
|
|
710
|
-
|
|
711
|
-
```html
|
|
712
|
-
<!-- Center aligned (default) -->
|
|
713
|
-
<web-multiselect badges-position="right" style="--ms-inline-align: center;"></web-multiselect>
|
|
714
|
-
|
|
715
|
-
<!-- Top aligned -->
|
|
716
|
-
<web-multiselect badges-position="right" style="--ms-inline-align: flex-start;"></web-multiselect>
|
|
717
|
-
|
|
718
|
-
<!-- Bottom aligned -->
|
|
719
|
-
<web-multiselect badges-position="left" style="--ms-inline-align: flex-end;"></web-multiselect>
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
**Note:** In RTL mode, left/right positions are automatically mirrored - `badges-position="left"` will appear on the physical right side in RTL languages.
|
|
723
|
-
|
|
724
|
-
### Badge Tooltips
|
|
725
|
-
|
|
726
|
-
Enable tooltips on selected item badges with customizable placement and delay:
|
|
727
|
-
|
|
728
|
-
```html
|
|
729
|
-
<!-- Basic tooltips -->
|
|
730
|
-
<web-multiselect
|
|
731
|
-
enable-badge-tooltips="true"
|
|
732
|
-
badge-tooltip-placement="top">
|
|
733
|
-
</web-multiselect>
|
|
734
|
-
|
|
735
|
-
<!-- Fast tooltips with custom delay -->
|
|
736
|
-
<web-multiselect
|
|
737
|
-
enable-badge-tooltips="true"
|
|
738
|
-
badge-tooltip-delay="100">
|
|
739
|
-
</web-multiselect>
|
|
740
|
-
|
|
741
|
-
<script type="module">
|
|
742
|
-
const select = document.querySelector('web-multiselect');
|
|
743
|
-
|
|
744
|
-
// Custom tooltip content
|
|
745
|
-
select.getBadgeTooltipCallback = (item) => {
|
|
746
|
-
return `${item.label} - ${item.subtitle}`;
|
|
747
|
-
};
|
|
748
|
-
</script>
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
### Internationalization (i18n)
|
|
752
|
-
|
|
753
|
-
Customize counter text for proper pluralization and localization:
|
|
754
|
-
|
|
755
|
-
```html
|
|
756
|
-
<web-multiselect
|
|
757
|
-
id="i18n-select"
|
|
758
|
-
badges-threshold="5"
|
|
759
|
-
badges-threshold-mode="partial"
|
|
760
|
-
badges-max-visible="3">
|
|
761
|
-
</web-multiselect>
|
|
762
|
-
|
|
763
|
-
<script type="module">
|
|
764
|
-
const select = document.getElementById('i18n-select');
|
|
765
|
-
|
|
766
|
-
// Spanish pluralization example
|
|
767
|
-
select.getCounterCallback = (count, moreCount) => {
|
|
768
|
-
if (moreCount !== undefined) {
|
|
769
|
-
// Partial mode: "+X more" badge
|
|
770
|
-
return moreCount === 1 ? '+1 más' : `+${moreCount} más`;
|
|
771
|
-
}
|
|
772
|
-
// Count mode: total count
|
|
773
|
-
return count === 1 ? '1 elemento seleccionado' : `${count} elementos seleccionados`;
|
|
774
|
-
};
|
|
775
|
-
</script>
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
### Right-to-Left (RTL) Language Support
|
|
779
|
-
|
|
780
|
-
Full RTL support for Arabic, Hebrew, Persian, Urdu, and other right-to-left languages with automatic detection and complete UI mirroring:
|
|
781
|
-
|
|
782
|
-
```html
|
|
783
|
-
<!-- Automatic RTL detection from dir attribute -->
|
|
784
|
-
<web-multiselect dir="rtl" search-placeholder="ابحث..."></web-multiselect>
|
|
785
|
-
|
|
786
|
-
<!-- RTL inherited from parent element -->
|
|
787
|
-
<div dir="rtl">
|
|
788
|
-
<web-multiselect search-placeholder="חיפוש..."></web-multiselect>
|
|
789
|
-
</div>
|
|
790
|
-
|
|
791
|
-
<!-- RTL on page level -->
|
|
792
|
-
<html dir="rtl">
|
|
793
|
-
<!-- All multi-selects will auto-detect RTL -->
|
|
794
|
-
</html>
|
|
42
|
+
```bash
|
|
43
|
+
npm install @keenmate/web-multiselect
|
|
795
44
|
```
|
|
796
45
|
|
|
797
|
-
|
|
798
|
-
- ✅ **Auto-detection** - Detects `dir="rtl"` on component or any ancestor element
|
|
799
|
-
- ✅ **Complete UI mirroring** - Toggle icon, text alignment, badges, dropdown, badges
|
|
800
|
-
- ✅ **Logical positioning** - `badges-position="left"` becomes physically right in RTL
|
|
801
|
-
- ✅ **Badge remove buttons** - Flip to left side in RTL mode
|
|
802
|
-
- ✅ **Text direction** - All text content properly right-aligned
|
|
803
|
-
- ✅ **No configuration needed** - Just set `dir="rtl"` attribute
|
|
804
|
-
|
|
805
|
-
### Custom Rendering
|
|
806
|
-
|
|
807
|
-
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.
|
|
808
|
-
|
|
809
|
-
#### Overview
|
|
810
|
-
|
|
811
|
-
Three rendering callbacks are available:
|
|
812
|
-
- **`renderOptionContentCallback`** - Customize dropdown option content
|
|
813
|
-
- **`renderBadgeContentCallback`** - Customize badge (selected item) content
|
|
814
|
-
- **`renderSelectedContentCallback`** - Customize selected value text (single-select mode)
|
|
815
|
-
|
|
816
|
-
All callbacks can return either **HTML strings** or **HTMLElement** objects (except `renderSelectedContentCallback` which returns plain text).
|
|
817
|
-
|
|
818
|
-
#### HTML Injection (XSS) Notice
|
|
46
|
+
## Quick start
|
|
819
47
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
| Callback | Output Used In | Risk Level |
|
|
823
|
-
|----------|---------------|------------|
|
|
824
|
-
| `renderOptionContentCallback` | Dropdown options (innerHTML) | HTML injection |
|
|
825
|
-
| `renderBadgeContentCallback` | Badges (innerHTML) | HTML injection |
|
|
826
|
-
| `renderSelectedItemContentCallback` | Selected items popover (innerHTML) | HTML injection |
|
|
827
|
-
| `renderGroupLabelContentCallback` | Group headers (innerHTML) | HTML injection |
|
|
828
|
-
| `getIconCallback` | Option icons (innerHTML) | HTML injection |
|
|
829
|
-
| `getSubtitleCallback` | Option subtitles (innerHTML) | HTML injection |
|
|
830
|
-
| `getDisplayValueCallback` | Option titles, badges (innerHTML) | HTML injection |
|
|
831
|
-
| `getBadgeDisplayCallback` | Badge text (innerHTML) | HTML injection |
|
|
832
|
-
| `getCounterCallback` | Count badges (innerHTML) | HTML injection |
|
|
833
|
-
| `getBadgeTooltipCallback` | Tooltips (innerHTML if HTMLElement) | HTML injection |
|
|
834
|
-
| `customStylesCallback` | Style tag (textContent) | CSS injection |
|
|
835
|
-
|
|
836
|
-
**Safe callbacks** (output is escaped or used as data):
|
|
837
|
-
- `getValueCallback`, `getSearchValueCallback`, `getGroupCallback`, `getDisabledCallback`
|
|
838
|
-
- `getBadgeClassCallback`, `getSelectedItemClassCallback` (CSS class names only)
|
|
839
|
-
- `beforeSearchCallback`, `searchCallback`, `addNewCallback`
|
|
840
|
-
- `selectCallback`, `deselectCallback`, `changeCallback`
|
|
841
|
-
- `getRemoveButtonTooltipCallback` (used as title attribute)
|
|
842
|
-
- `getValueFormatCallback` (form value)
|
|
843
|
-
|
|
844
|
-
**If displaying user-generated content**, sanitize it before returning from these callbacks.
|
|
845
|
-
|
|
846
|
-
#### Custom Option Rendering
|
|
847
|
-
|
|
848
|
-
Customize how options appear in the dropdown:
|
|
48
|
+
**Declarative — no JavaScript required:**
|
|
849
49
|
|
|
850
50
|
```html
|
|
851
|
-
<web-multiselect id="custom-options"></web-multiselect>
|
|
852
|
-
|
|
853
51
|
<script type="module">
|
|
854
52
|
import '@keenmate/web-multiselect';
|
|
855
|
-
|
|
856
|
-
const select = document.getElementById('custom-options');
|
|
857
|
-
|
|
858
|
-
select.options = [
|
|
859
|
-
{ id: 1, name: 'React', stars: 220000, trending: true },
|
|
860
|
-
{ id: 2, name: 'Vue', stars: 207000, trending: false },
|
|
861
|
-
{ id: 3, name: 'Angular', stars: 94000, trending: false },
|
|
862
|
-
{ id: 4, name: 'Svelte', stars: 76000, trending: true }
|
|
863
|
-
];
|
|
864
|
-
|
|
865
|
-
// Custom renderer with full HTML control
|
|
866
|
-
select.renderOptionContentCallback = (item, context) => {
|
|
867
|
-
// Context provides: { index, isSelected, isFocused, isMatched, isDisabled }
|
|
868
|
-
|
|
869
|
-
return `
|
|
870
|
-
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
871
|
-
<strong>${item.name}</strong>
|
|
872
|
-
<span style="color: #666; font-size: 0.875rem;">⭐ ${(item.stars / 1000).toFixed(0)}k</span>
|
|
873
|
-
${item.trending ? '<span style="background: #10b981; color: white; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.75rem;">🔥 Trending</span>' : ''}
|
|
874
|
-
</div>
|
|
875
|
-
`;
|
|
876
|
-
};
|
|
877
|
-
</script>
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
**Context object** (`OptionContentRenderContext`):
|
|
881
|
-
- `index: number` - Index of the option in the filtered list
|
|
882
|
-
- `isSelected: boolean` - Whether the option is currently selected
|
|
883
|
-
- `isFocused: boolean` - Whether the option is currently focused (keyboard navigation)
|
|
884
|
-
- `isMatched: boolean` - Whether the option matches the current search term (navigate mode only)
|
|
885
|
-
- `isDisabled: boolean` - Whether the option is disabled
|
|
886
|
-
|
|
887
|
-
#### Custom Badge Rendering
|
|
888
|
-
|
|
889
|
-
Customize how selected items appear as badges:
|
|
890
|
-
|
|
891
|
-
```javascript
|
|
892
|
-
const select = document.querySelector('web-multiselect');
|
|
893
|
-
|
|
894
|
-
select.options = [
|
|
895
|
-
{ id: 1, name: 'John Doe', role: 'Admin', avatar: '👨💼' },
|
|
896
|
-
{ id: 2, name: 'Jane Smith', role: 'Developer', avatar: '👩💻' },
|
|
897
|
-
{ id: 3, name: 'Bob Johnson', role: 'Designer', avatar: '🎨' }
|
|
898
|
-
];
|
|
899
|
-
|
|
900
|
-
// Custom badge rendering in main badges area
|
|
901
|
-
select.renderBadgeContentCallback = (item, context) => {
|
|
902
|
-
// Compact view in badges area
|
|
903
|
-
return `${item.avatar} ${item.name}`;
|
|
904
|
-
};
|
|
905
|
-
|
|
906
|
-
// Custom rendering for selected items popover (separate callback)
|
|
907
|
-
select.renderSelectedItemContentCallback = (item) => {
|
|
908
|
-
// Full details in popover - has more space
|
|
909
|
-
return `
|
|
910
|
-
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
911
|
-
<span>${item.avatar}</span>
|
|
912
|
-
<div>
|
|
913
|
-
<div><strong>${item.name}</strong></div>
|
|
914
|
-
<div style="font-size: 0.75rem; color: #666;">${item.role}</div>
|
|
915
|
-
</div>
|
|
916
|
-
</div>
|
|
917
|
-
`;
|
|
918
|
-
};
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
**Separate Callbacks for Badges vs. Popover:**
|
|
922
|
-
- `renderBadgeContentCallback` - Renders badges in the main badges area (compact display)
|
|
923
|
-
- `renderSelectedItemContentCallback` - Renders items in the selected items popover (can be more detailed)
|
|
924
|
-
- If `renderSelectedItemContentCallback` is not defined, falls back to `renderBadgeContentCallback`
|
|
925
|
-
- Users can assign the same function to both if identical rendering is desired
|
|
926
|
-
|
|
927
|
-
**Context object** (`BadgeContentRenderContext` for `renderBadgeContentCallback`):
|
|
928
|
-
- `displayMode: BadgesDisplayMode` - Current badges display mode ('pills', 'count', 'compact', 'partial', 'none')
|
|
929
|
-
- `isInPopover: boolean` - Whether the badge is being rendered in the selected items popover (always false for this callback)
|
|
930
|
-
|
|
931
|
-
#### Custom Group Label Rendering
|
|
932
|
-
|
|
933
|
-
Customize how group headers are displayed using `renderGroupLabelContentCallback`:
|
|
934
|
-
|
|
935
|
-
```javascript
|
|
936
|
-
const select = document.querySelector('web-multiselect');
|
|
937
|
-
|
|
938
|
-
select.options = [
|
|
939
|
-
{ value: 'react', label: 'React', group: 'frontend' },
|
|
940
|
-
{ value: 'vue', label: 'Vue', group: 'frontend' },
|
|
941
|
-
{ value: 'nodejs', label: 'Node.js', group: 'backend' },
|
|
942
|
-
{ value: 'postgres', label: 'PostgreSQL', group: 'database' }
|
|
943
|
-
];
|
|
944
|
-
|
|
945
|
-
select.isGroupsAllowed = true;
|
|
946
|
-
select.groupMember = 'group';
|
|
947
|
-
|
|
948
|
-
// Customize group label display
|
|
949
|
-
select.renderGroupLabelContentCallback = (groupName) => {
|
|
950
|
-
const emojis = {
|
|
951
|
-
'frontend': '🎨',
|
|
952
|
-
'backend': '🔧',
|
|
953
|
-
'database': '🗄️'
|
|
954
|
-
};
|
|
955
|
-
const emoji = emojis[groupName] || '📦';
|
|
956
|
-
return `<strong>${emoji} ${groupName.toUpperCase()}</strong>`;
|
|
957
|
-
};
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
**Signature:** `(groupName: string) => string | HTMLElement`
|
|
961
|
-
|
|
962
|
-
**Use cases:**
|
|
963
|
-
- Capitalize or format group names
|
|
964
|
-
- Add icons, emojis, or badges to group headers
|
|
965
|
-
- Apply HTML formatting (bold, colors, etc.)
|
|
966
|
-
- Internationalization (i18n) - translate group names
|
|
967
|
-
- Add group-specific metadata or counts
|
|
968
|
-
|
|
969
|
-
**Notes:**
|
|
970
|
-
- Keeps standard `.ms__group-label` wrapper for consistent styling
|
|
971
|
-
- Can return HTML string or HTMLElement
|
|
972
|
-
- Group name is passed as a string parameter
|
|
973
|
-
|
|
974
|
-
#### Custom Badge Styling with CSS Classes
|
|
975
|
-
|
|
976
|
-
Add custom CSS classes to badges based on item data for semantic styling:
|
|
977
|
-
|
|
978
|
-
```javascript
|
|
979
|
-
const select = document.querySelector('web-multiselect');
|
|
980
|
-
|
|
981
|
-
select.options = [
|
|
982
|
-
{ id: 1, task: 'Fix security bug', priority: 'urgent' },
|
|
983
|
-
{ id: 2, task: 'Update docs', priority: 'normal' },
|
|
984
|
-
{ id: 3, task: 'Refactor code', priority: 'low' }
|
|
985
|
-
];
|
|
986
|
-
|
|
987
|
-
// Add CSS class based on priority
|
|
988
|
-
select.getBadgeClassCallback = (item) => {
|
|
989
|
-
return `badge-${item.priority}`; // Returns 'badge-urgent', 'badge-normal', etc.
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
// Can also return array of classes
|
|
993
|
-
select.getBadgeClassCallback = (item) => {
|
|
994
|
-
const classes = [`badge-${item.priority}`];
|
|
995
|
-
if (item.urgent) classes.push('badge-blink');
|
|
996
|
-
return classes;
|
|
997
|
-
};
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
Then style with CSS:
|
|
1001
|
-
|
|
1002
|
-
```css
|
|
1003
|
-
/* Target specific badges with custom classes */
|
|
1004
|
-
.badge-urgent {
|
|
1005
|
-
--ms-badge-text-bg: #fee2e2;
|
|
1006
|
-
--ms-badge-text-color: #dc2626;
|
|
1007
|
-
--ms-badge-remove-bg: #dc2626;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
.badge-normal {
|
|
1011
|
-
--ms-badge-text-bg: #dbeafe;
|
|
1012
|
-
--ms-badge-text-color: #2563eb;
|
|
1013
|
-
--ms-badge-remove-bg: #2563eb;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
.badge-low {
|
|
1017
|
-
--ms-badge-text-bg: #d1fae5;
|
|
1018
|
-
--ms-badge-text-color: #059669;
|
|
1019
|
-
--ms-badge-remove-bg: #059669;
|
|
1020
|
-
}
|
|
1021
|
-
```
|
|
1022
|
-
|
|
1023
|
-
The callback:
|
|
1024
|
-
- Takes the item as a parameter
|
|
1025
|
-
- Returns a string (single class) or array of strings (multiple classes)
|
|
1026
|
-
- Classes are added to the badge's base `.ml__badge` element
|
|
1027
|
-
- Works across all rendering locations (main badges, partial mode, popover)
|
|
1028
|
-
|
|
1029
|
-
**Separate Class Callbacks for Badges vs. Popover:**
|
|
1030
|
-
|
|
1031
|
-
Similar to rendering callbacks, you can use different class callbacks for badges and selected items:
|
|
1032
|
-
|
|
1033
|
-
```javascript
|
|
1034
|
-
// Add classes to badges in main area
|
|
1035
|
-
select.getBadgeClassCallback = (item) => {
|
|
1036
|
-
return `badge-${item.priority}`;
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1039
|
-
// Add different/additional classes to selected items in popover
|
|
1040
|
-
select.getSelectedItemClassCallback = (item) => {
|
|
1041
|
-
// Could add more detailed classes for popover items
|
|
1042
|
-
return [`badge-${item.priority}`, 'badge-detailed'];
|
|
1043
|
-
};
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
- `getBadgeClassCallback` - Adds classes to badges in the main badges area
|
|
1047
|
-
- `getSelectedItemClassCallback` - Adds classes to items in the selected items popover
|
|
1048
|
-
- If `getSelectedItemClassCallback` is not defined, falls back to `getBadgeClassCallback`
|
|
1049
|
-
- Users can assign the same function to both if identical styling is desired
|
|
1050
|
-
|
|
1051
|
-
**Shadow DOM CSS Injection:**
|
|
1052
|
-
|
|
1053
|
-
Since the component uses Shadow DOM, regular page CSS cannot style shadow elements. Use `customStylesCallback` to inject CSS directly into the Shadow DOM:
|
|
1054
|
-
|
|
1055
|
-
```javascript
|
|
1056
|
-
const select = document.querySelector('web-multiselect');
|
|
1057
|
-
|
|
1058
|
-
// Add CSS classes to badges based on item data
|
|
1059
|
-
select.getBadgeClassCallback = (item) => {
|
|
1060
|
-
return `badge-${item.priority}`;
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
|
-
// Inject CSS into Shadow DOM to style those classes
|
|
1064
|
-
select.customStylesCallback = () => `
|
|
1065
|
-
.badge-urgent {
|
|
1066
|
-
--ms-badge-text-bg: #fee2e2;
|
|
1067
|
-
--ms-badge-text-color: #dc2626;
|
|
1068
|
-
--ms-badge-remove-bg: #dc2626;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
.badge-normal {
|
|
1072
|
-
--ms-badge-text-bg: #dbeafe;
|
|
1073
|
-
--ms-badge-text-color: #2563eb;
|
|
1074
|
-
--ms-badge-remove-bg: #2563eb;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
.badge-low {
|
|
1078
|
-
--ms-badge-text-bg: #d1fae5;
|
|
1079
|
-
--ms-badge-text-color: #059669;
|
|
1080
|
-
--ms-badge-remove-bg: #059669;
|
|
1081
|
-
}
|
|
1082
|
-
`;
|
|
1083
|
-
```
|
|
1084
|
-
|
|
1085
|
-
The `customStylesCallback`:
|
|
1086
|
-
- Returns a CSS string (not HTML)
|
|
1087
|
-
- Styles are injected into the Shadow DOM on initialization
|
|
1088
|
-
- Can be updated dynamically - new styles replace old ones
|
|
1089
|
-
- Works with all custom classes (from `getBadgeClassCallback`, `renderOptionContentCallback`, etc.)
|
|
1090
|
-
|
|
1091
|
-
#### Custom Selected Item Rendering (Single-Select)
|
|
1092
|
-
|
|
1093
|
-
Customize the text shown in the input field when in single-select mode:
|
|
1094
|
-
|
|
1095
|
-
```javascript
|
|
1096
|
-
const select = document.querySelector('web-multiselect[multiple="false"]');
|
|
1097
|
-
|
|
1098
|
-
select.options = [
|
|
1099
|
-
{ id: 1, firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
|
|
1100
|
-
{ id: 2, firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' }
|
|
1101
|
-
];
|
|
1102
|
-
|
|
1103
|
-
// Show just first name when closed
|
|
1104
|
-
select.renderSelectedContentCallback = (item) => {
|
|
1105
|
-
return item.firstName; // Returns plain text (not HTML)
|
|
1106
|
-
};
|
|
1107
|
-
|
|
1108
|
-
// While dropdown shows full details
|
|
1109
|
-
select.getDisplayValueCallback = (item) => {
|
|
1110
|
-
return `${item.firstName} ${item.lastName} (${item.email})`;
|
|
1111
|
-
};
|
|
1112
|
-
```
|
|
1113
|
-
|
|
1114
|
-
#### Conditional Rendering Example
|
|
1115
|
-
|
|
1116
|
-
Use JavaScript logic for conditional rendering:
|
|
1117
|
-
|
|
1118
|
-
```javascript
|
|
1119
|
-
select.renderOptionContentCallback = (item, context) => {
|
|
1120
|
-
const classes = [];
|
|
1121
|
-
if (context.isSelected) classes.push('selected');
|
|
1122
|
-
if (context.isFocused) classes.push('focused');
|
|
1123
|
-
|
|
1124
|
-
return `
|
|
1125
|
-
<div class="${classes.join(' ')}">
|
|
1126
|
-
${item.isNew ? '<span class="badge-new">NEW</span>' : ''}
|
|
1127
|
-
<strong>${item.name}</strong>
|
|
1128
|
-
${item.description ? `<p style="font-size: 0.875rem; color: #666;">${item.description}</p>` : ''}
|
|
1129
|
-
${item.tags ? `<div class="tags">${item.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}</div>` : ''}
|
|
1130
|
-
</div>
|
|
1131
|
-
`;
|
|
1132
|
-
};
|
|
1133
|
-
```
|
|
1134
|
-
|
|
1135
|
-
#### Returning HTMLElement
|
|
1136
|
-
|
|
1137
|
-
You can also return DOM elements for more complex rendering:
|
|
1138
|
-
|
|
1139
|
-
```javascript
|
|
1140
|
-
select.renderOptionContentCallback = (item, context) => {
|
|
1141
|
-
const div = document.createElement('div');
|
|
1142
|
-
div.style.display = 'flex';
|
|
1143
|
-
div.style.alignItems = 'center';
|
|
1144
|
-
div.style.gap = '0.5rem';
|
|
1145
|
-
|
|
1146
|
-
const img = document.createElement('img');
|
|
1147
|
-
img.src = item.avatarUrl;
|
|
1148
|
-
img.style.width = '32px';
|
|
1149
|
-
img.style.height = '32px';
|
|
1150
|
-
img.style.borderRadius = '50%';
|
|
1151
|
-
|
|
1152
|
-
const span = document.createElement('span');
|
|
1153
|
-
span.textContent = item.name;
|
|
1154
|
-
|
|
1155
|
-
div.appendChild(img);
|
|
1156
|
-
div.appendChild(span);
|
|
1157
|
-
|
|
1158
|
-
return div; // Return HTMLElement instead of string
|
|
1159
|
-
};
|
|
1160
|
-
```
|
|
1161
|
-
|
|
1162
|
-
#### Virtual Scroll Compatibility
|
|
1163
|
-
|
|
1164
|
-
When using `renderOptionContentCallback` with virtual scroll enabled:
|
|
1165
|
-
|
|
1166
|
-
⚠️ **Important**: Custom option content **must fit within** the configured `optionHeight` (default: 50px)
|
|
1167
|
-
|
|
1168
|
-
```html
|
|
1169
|
-
<web-multiselect
|
|
1170
|
-
id="large-dataset"
|
|
1171
|
-
enable-virtual-scroll="true"
|
|
1172
|
-
option-height="60">
|
|
1173
|
-
</web-multiselect>
|
|
1174
|
-
|
|
1175
|
-
<script type="module">
|
|
1176
|
-
const select = document.getElementById('large-dataset');
|
|
1177
|
-
|
|
1178
|
-
select.renderOptionContentCallback = (item) => {
|
|
1179
|
-
// Content must fit in 60px height
|
|
1180
|
-
return `
|
|
1181
|
-
<div style="height: 60px; display: flex; align-items: center;">
|
|
1182
|
-
<strong>${item.name}</strong>
|
|
1183
|
-
</div>
|
|
1184
|
-
`;
|
|
1185
|
-
};
|
|
1186
53
|
</script>
|
|
1187
|
-
```
|
|
1188
|
-
|
|
1189
|
-
**Virtual scroll requirements:**
|
|
1190
|
-
- Content height must be **fixed** and match `optionHeight`
|
|
1191
|
-
- Overflow will be clipped
|
|
1192
|
-
- Variable-height content only works in non-virtual mode
|
|
1193
|
-
|
|
1194
|
-
#### Callback Priority
|
|
1195
|
-
|
|
1196
|
-
The component uses a fallback chain when callbacks are not provided:
|
|
1197
|
-
|
|
1198
|
-
**For options:**
|
|
1199
|
-
1. `renderOptionContentCallback` (full HTML control)
|
|
1200
|
-
2. Default: icon + `getDisplayValueCallback` + subtitle
|
|
1201
|
-
|
|
1202
|
-
**For badges:**
|
|
1203
|
-
1. `renderBadgeContentCallback` (full HTML control)
|
|
1204
|
-
2. `getBadgeDisplayCallback` (text only)
|
|
1205
|
-
3. `getDisplayValueCallback` (text only)
|
|
1206
|
-
|
|
1207
|
-
**For selected item (single-select):**
|
|
1208
|
-
1. `renderSelectedContentCallback` (text only)
|
|
1209
|
-
2. `getDisplayValueCallback` (text only)
|
|
1210
|
-
|
|
1211
|
-
#### Checkbox Control
|
|
1212
|
-
|
|
1213
|
-
Control checkbox appearance and alignment with CSS variables and attributes:
|
|
1214
|
-
|
|
1215
|
-
**Checkbox Alignment (via attribute):**
|
|
1216
|
-
```html
|
|
1217
|
-
<web-multiselect checkbox-align="top"></web-multiselect> <!-- Default -->
|
|
1218
|
-
<web-multiselect checkbox-align="center"></web-multiselect> <!-- Middle aligned -->
|
|
1219
|
-
<web-multiselect checkbox-align="bottom"></web-multiselect> <!-- Bottom aligned -->
|
|
1220
|
-
```
|
|
1221
|
-
|
|
1222
|
-
**Checkbox Size/Scale (via CSS):**
|
|
1223
|
-
```html
|
|
1224
|
-
<style>
|
|
1225
|
-
/* Change checkbox size */
|
|
1226
|
-
web-multiselect {
|
|
1227
|
-
--ms-checkbox-size: 20px; /* Width and height (default: 16px) */
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
/* Scale checkbox */
|
|
1231
|
-
web-multiselect {
|
|
1232
|
-
--ms-checkbox-scale: 1.5; /* Scale multiplier (default: 1) */
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
/* Fine-tune checkbox positioning */
|
|
1236
|
-
web-multiselect {
|
|
1237
|
-
--ms-checkbox-margin-top: 0.5rem; /* Vertical alignment (default: 0.125rem) */
|
|
1238
|
-
--ms-checkbox-margin-right: 0; /* Right spacing (default: 0) */
|
|
1239
|
-
--ms-checkbox-margin-bottom: 0; /* Bottom spacing (default: 0) */
|
|
1240
|
-
--ms-checkbox-margin-left: 0; /* Left spacing (default: 0) */
|
|
1241
|
-
}
|
|
1242
|
-
</style>
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
**CSS Grid/Flexbox in Custom Content:**
|
|
1246
|
-
|
|
1247
|
-
Custom rendering callbacks support full CSS layout control:
|
|
1248
|
-
|
|
1249
|
-
```javascript
|
|
1250
|
-
// CSS Grid example
|
|
1251
|
-
multiselect.renderOptionContentCallback = (item, context) => {
|
|
1252
|
-
return `
|
|
1253
|
-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
|
|
1254
|
-
<div><strong>Name:</strong> ${item.name}</div>
|
|
1255
|
-
<div><strong>Price:</strong> ${item.price}</div>
|
|
1256
|
-
<div><strong>Stock:</strong> ${item.stock}</div>
|
|
1257
|
-
<div><strong>Rating:</strong> ${item.rating}</div>
|
|
1258
|
-
</div>
|
|
1259
|
-
`;
|
|
1260
|
-
};
|
|
1261
|
-
|
|
1262
|
-
// Flexbox example
|
|
1263
|
-
multiselect.renderOptionContentCallback = (item, context) => {
|
|
1264
|
-
return `
|
|
1265
|
-
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
1266
|
-
<div style="display: flex; flex-direction: column;">
|
|
1267
|
-
<strong>${item.name}</strong>
|
|
1268
|
-
<span style="font-size: 0.875rem; color: #666;">${item.description}</span>
|
|
1269
|
-
</div>
|
|
1270
|
-
<div style="text-align: right;">
|
|
1271
|
-
<div>${item.price}</div>
|
|
1272
|
-
<div style="font-size: 0.875rem;">${item.stock} in stock</div>
|
|
1273
|
-
</div>
|
|
1274
|
-
</div>
|
|
1275
|
-
`;
|
|
1276
|
-
};
|
|
1277
|
-
```
|
|
1278
|
-
|
|
1279
|
-
**Available CSS Variables:**
|
|
1280
|
-
- `--ms-checkbox-size`: Checkbox width/height (default: `16px`)
|
|
1281
|
-
- `--ms-checkbox-scale`: Scale multiplier (default: `1`)
|
|
1282
|
-
- `--ms-checkbox-margin-top`: Top margin for vertical alignment (default: `0.125rem`)
|
|
1283
|
-
- `--ms-checkbox-margin-right`: Right margin (default: `0`)
|
|
1284
|
-
- `--ms-checkbox-margin-bottom`: Bottom margin (default: `0`)
|
|
1285
|
-
- `--ms-checkbox-margin-left`: Left margin (default: `0`)
|
|
1286
|
-
- `--ms-checkbox-align`: Alignment value (default: `flex-start`)
|
|
1287
|
-
- `--ms-option-gap`: Gap between checkbox and content (default: `0.5rem`)
|
|
1288
|
-
|
|
1289
|
-
**Note:** Horizontal and bottom margins default to `0` since spacing is handled by flexbox gap. Override for custom layouts.
|
|
1290
|
-
|
|
1291
|
-
### Flexible Data Handling
|
|
1292
|
-
|
|
1293
|
-
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.
|
|
1294
|
-
|
|
1295
|
-
#### Member Properties (Simple Property Names)
|
|
1296
54
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
<
|
|
1301
|
-
id="products"
|
|
1302
|
-
value-member="productId"
|
|
1303
|
-
display-value-member="productName"
|
|
1304
|
-
icon-member="icon"
|
|
1305
|
-
subtitle-member="description"
|
|
1306
|
-
group-member="category">
|
|
55
|
+
<web-multiselect placeholder="Pick a country">
|
|
56
|
+
<option value="cz">Czech Republic</option>
|
|
57
|
+
<option value="sk">Slovakia</option>
|
|
58
|
+
<option value="at">Austria</option>
|
|
1307
59
|
</web-multiselect>
|
|
1308
|
-
|
|
1309
|
-
<script type="module">
|
|
1310
|
-
const select = document.getElementById('products');
|
|
1311
|
-
select.options = [
|
|
1312
|
-
{
|
|
1313
|
-
productId: 'p1',
|
|
1314
|
-
productName: 'Laptop',
|
|
1315
|
-
icon: '💻',
|
|
1316
|
-
description: 'High-performance laptop',
|
|
1317
|
-
category: 'Electronics'
|
|
1318
|
-
},
|
|
1319
|
-
{
|
|
1320
|
-
productId: 'p2',
|
|
1321
|
-
productName: 'Mouse',
|
|
1322
|
-
icon: '🖱️',
|
|
1323
|
-
description: 'Wireless mouse',
|
|
1324
|
-
category: 'Electronics'
|
|
1325
|
-
}
|
|
1326
|
-
];
|
|
1327
|
-
</script>
|
|
1328
60
|
```
|
|
1329
61
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
For complex data extraction or conditional logic, use callbacks:
|
|
1333
|
-
|
|
1334
|
-
```javascript
|
|
1335
|
-
const select = document.querySelector('web-multiselect');
|
|
1336
|
-
|
|
1337
|
-
// Custom value extraction
|
|
1338
|
-
select.getValueCallback = (item) => item.id || item.code || item.value;
|
|
1339
|
-
|
|
1340
|
-
// Combine multiple fields for display
|
|
1341
|
-
select.getDisplayValueCallback = (item) => {
|
|
1342
|
-
return `${item.firstName} ${item.lastName}`;
|
|
1343
|
-
};
|
|
1344
|
-
|
|
1345
|
-
// Include multiple fields in search
|
|
1346
|
-
select.getSearchValueCallback = (item) => {
|
|
1347
|
-
return `${item.name} ${item.sku} ${item.tags.join(' ')}`;
|
|
1348
|
-
};
|
|
1349
|
-
|
|
1350
|
-
// Conditional icons
|
|
1351
|
-
select.getIconCallback = (item) => {
|
|
1352
|
-
return item.inStock ? '✅' : '❌';
|
|
1353
|
-
};
|
|
1354
|
-
|
|
1355
|
-
// Dynamic subtitles
|
|
1356
|
-
select.getSubtitleCallback = (item) => {
|
|
1357
|
-
return `$${item.price} - ${item.stock} in stock`;
|
|
1358
|
-
};
|
|
1359
|
-
|
|
1360
|
-
// Disable based on conditions
|
|
1361
|
-
select.getDisabledCallback = (item) => {
|
|
1362
|
-
return item.stock === 0 || item.discontinued;
|
|
1363
|
-
};
|
|
1364
|
-
|
|
1365
|
-
// Customize badge display (show different text in badges vs dropdown)
|
|
1366
|
-
select.getBadgeDisplayCallback = (item) => {
|
|
1367
|
-
// Badges show just the name for space efficiency
|
|
1368
|
-
return item.name;
|
|
1369
|
-
// While dropdown can show full details: "Laptop - $999 - Electronics"
|
|
1370
|
-
};
|
|
1371
|
-
```
|
|
1372
|
-
|
|
1373
|
-
#### Tuple Array Auto-Detection
|
|
1374
|
-
|
|
1375
|
-
The component automatically detects `[key, value]` tuple arrays:
|
|
1376
|
-
|
|
1377
|
-
```javascript
|
|
1378
|
-
select.options = [
|
|
1379
|
-
['js', 'JavaScript'],
|
|
1380
|
-
['ts', 'TypeScript'],
|
|
1381
|
-
['py', 'Python']
|
|
1382
|
-
];
|
|
1383
|
-
// First element becomes value, second becomes display text
|
|
1384
|
-
```
|
|
1385
|
-
|
|
1386
|
-
#### Priority Order
|
|
1387
|
-
|
|
1388
|
-
When multiple extraction methods are defined, the component uses this priority:
|
|
1389
|
-
|
|
1390
|
-
1. **Callbacks** (highest priority) - `getValueCallback`, `getDisplayValueCallback`, etc.
|
|
1391
|
-
2. **Member properties** - `valueMember`, `displayValueMember`, etc.
|
|
1392
|
-
3. **Default properties** (lowest priority) - Falls back to `value`, `label`, `name`, etc.
|
|
1393
|
-
|
|
1394
|
-
#### TypeScript Support
|
|
1395
|
-
|
|
1396
|
-
The component is fully typed with generics:
|
|
1397
|
-
|
|
1398
|
-
```typescript
|
|
1399
|
-
import type { MultiSelectElement } from '@keenmate/web-multiselect';
|
|
1400
|
-
|
|
1401
|
-
interface Product {
|
|
1402
|
-
id: string;
|
|
1403
|
-
name: string;
|
|
1404
|
-
price: number;
|
|
1405
|
-
category: string;
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
const select = document.querySelector<MultiSelectElement<Product>>('web-multiselect');
|
|
1409
|
-
select.options = [
|
|
1410
|
-
{ id: 'p1', name: 'Laptop', price: 999, category: 'Electronics' }
|
|
1411
|
-
];
|
|
1412
|
-
```
|
|
1413
|
-
|
|
1414
|
-
### Form Integration
|
|
1415
|
-
|
|
1416
|
-
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.
|
|
1417
|
-
|
|
1418
|
-
#### Basic Form Integration
|
|
62
|
+
**Programmatic — dynamic data + events:**
|
|
1419
63
|
|
|
1420
64
|
```html
|
|
1421
|
-
<
|
|
1422
|
-
<label>Select Skills:</label>
|
|
1423
|
-
<web-multiselect
|
|
1424
|
-
name="skills"
|
|
1425
|
-
value-format="json"
|
|
1426
|
-
multiple="true">
|
|
1427
|
-
</web-multiselect>
|
|
1428
|
-
|
|
1429
|
-
<button type="submit">Submit</button>
|
|
1430
|
-
</form>
|
|
65
|
+
<web-multiselect id="picker" search-placeholder="Search…"></web-multiselect>
|
|
1431
66
|
|
|
1432
67
|
<script type="module">
|
|
1433
68
|
import '@keenmate/web-multiselect';
|
|
1434
69
|
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
{ value: '
|
|
1440
|
-
{ value: 'ts', label: 'TypeScript' },
|
|
1441
|
-
{ value: 'py', label: 'Python' }
|
|
70
|
+
const picker = document.getElementById('picker');
|
|
71
|
+
picker.options = [
|
|
72
|
+
{ value: 'js', label: 'JavaScript', icon: '🟨' },
|
|
73
|
+
{ value: 'ts', label: 'TypeScript', icon: '🔷' },
|
|
74
|
+
{ value: 'py', label: 'Python', icon: '🐍' }
|
|
1442
75
|
];
|
|
1443
76
|
|
|
1444
|
-
|
|
1445
|
-
e.
|
|
1446
|
-
const formData = new FormData(form);
|
|
1447
|
-
|
|
1448
|
-
// Access the value
|
|
1449
|
-
const skills = formData.get('skills');
|
|
1450
|
-
console.log('Selected skills:', skills);
|
|
1451
|
-
// Output: ["js","ts"] (JSON string)
|
|
77
|
+
picker.addEventListener('change', (e) => {
|
|
78
|
+
console.log('Selected:', e.detail.selectedValues);
|
|
1452
79
|
});
|
|
1453
80
|
</script>
|
|
1454
81
|
```
|
|
1455
82
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
Choose how selected values are serialized in forms:
|
|
1459
|
-
|
|
1460
|
-
**JSON Format** (default):
|
|
1461
|
-
```html
|
|
1462
|
-
<web-multiselect name="items" value-format="json"></web-multiselect>
|
|
1463
|
-
<!-- FormData result: items = ["item1","item2","item3"] -->
|
|
1464
|
-
```
|
|
1465
|
-
|
|
1466
|
-
**CSV Format**:
|
|
1467
|
-
```html
|
|
1468
|
-
<web-multiselect name="items" value-format="csv"></web-multiselect>
|
|
1469
|
-
<!-- FormData result: items = "item1,item2,item3" -->
|
|
1470
|
-
```
|
|
1471
|
-
|
|
1472
|
-
**Array Format** (multiple inputs):
|
|
1473
|
-
```html
|
|
1474
|
-
<web-multiselect name="items" value-format="array"></web-multiselect>
|
|
1475
|
-
<!-- FormData result:
|
|
1476
|
-
items[] = "item1"
|
|
1477
|
-
items[] = "item2"
|
|
1478
|
-
items[] = "item3"
|
|
1479
|
-
-->
|
|
1480
|
-
```
|
|
1481
|
-
|
|
1482
|
-
#### Custom Value Formatting
|
|
1483
|
-
|
|
1484
|
-
For advanced use cases, provide a custom formatting function:
|
|
1485
|
-
|
|
1486
|
-
```javascript
|
|
1487
|
-
const select = document.querySelector('web-multiselect');
|
|
1488
|
-
|
|
1489
|
-
select.name = 'product_ids';
|
|
1490
|
-
select.getValueFormatCallback = (values) => {
|
|
1491
|
-
// Custom format: pipe-separated with prefix
|
|
1492
|
-
return values.map(v => `ID:${v}`).join('|');
|
|
1493
|
-
};
|
|
1494
|
-
|
|
1495
|
-
// When submitted, FormData will have:
|
|
1496
|
-
// product_ids = "ID:123|ID:456|ID:789"
|
|
1497
|
-
```
|
|
1498
|
-
|
|
1499
|
-
#### Using getValue() for JavaScript Submissions
|
|
1500
|
-
|
|
1501
|
-
For JavaScript-based form submissions (AJAX, fetch), use `getValue()`:
|
|
1502
|
-
|
|
1503
|
-
```javascript
|
|
1504
|
-
// Single-select mode
|
|
1505
|
-
const select = document.querySelector('multi-select[multiple="false"]');
|
|
1506
|
-
const selectedId = select.getValue();
|
|
1507
|
-
// Returns: "js" or null
|
|
1508
|
-
|
|
1509
|
-
// Multi-select mode
|
|
1510
|
-
const multiSelect = document.querySelector('multi-select[multiple="true"]');
|
|
1511
|
-
const selectedIds = multiSelect.getValue();
|
|
1512
|
-
// Returns: ["js", "ts", "py"] or []
|
|
1513
|
-
|
|
1514
|
-
// Submit with fetch
|
|
1515
|
-
const response = await fetch('/api/update', {
|
|
1516
|
-
method: 'POST',
|
|
1517
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1518
|
-
body: JSON.stringify({
|
|
1519
|
-
skills: multiSelect.getValue()
|
|
1520
|
-
})
|
|
1521
|
-
});
|
|
1522
|
-
```
|
|
1523
|
-
|
|
1524
|
-
#### Working with Numeric Values
|
|
1525
|
-
|
|
1526
|
-
The component handles both string and numeric values correctly:
|
|
1527
|
-
|
|
1528
|
-
```javascript
|
|
1529
|
-
select.options = [
|
|
1530
|
-
{ value: 1, label: 'Option 1' },
|
|
1531
|
-
{ value: 2, label: 'Option 2' },
|
|
1532
|
-
{ value: 3, label: 'Option 3' }
|
|
1533
|
-
];
|
|
1534
|
-
|
|
1535
|
-
// getValue() preserves types
|
|
1536
|
-
const values = select.getValue();
|
|
1537
|
-
// Returns: [1, 2, 3] (numbers, not strings)
|
|
1538
|
-
|
|
1539
|
-
// FormData serialization
|
|
1540
|
-
// JSON format: [1,2,3]
|
|
1541
|
-
// CSV format: 1,2,3
|
|
1542
|
-
// Array format: items[]=1, items[]=2, items[]=3
|
|
1543
|
-
```
|
|
1544
|
-
|
|
1545
|
-
### Disabled Options
|
|
1546
|
-
|
|
1547
|
-
```javascript
|
|
1548
|
-
select.options = [
|
|
1549
|
-
{ value: 'basic', label: 'Basic License', subtitle: 'Free forever' },
|
|
1550
|
-
{ value: 'pro', label: 'Pro License', subtitle: 'Available for purchase' },
|
|
1551
|
-
{
|
|
1552
|
-
value: 'enterprise',
|
|
1553
|
-
label: 'Enterprise License',
|
|
1554
|
-
subtitle: 'Contact sales',
|
|
1555
|
-
disabled: true
|
|
1556
|
-
}
|
|
1557
|
-
];
|
|
1558
|
-
```
|
|
1559
|
-
|
|
1560
|
-
## Option Structure
|
|
1561
|
-
|
|
1562
|
-
```typescript
|
|
1563
|
-
interface MultiSelectOption {
|
|
1564
|
-
value: string; // Required: Unique identifier
|
|
1565
|
-
label: string; // Required: Display text
|
|
1566
|
-
icon?: string; // Optional: Icon or emoji
|
|
1567
|
-
subtitle?: string; // Optional: Subtitle/description
|
|
1568
|
-
group?: string; // Optional: Group name
|
|
1569
|
-
disabled?: boolean; // Optional: Disable selection
|
|
1570
|
-
}
|
|
1571
|
-
```
|
|
1572
|
-
|
|
1573
|
-
## Styling
|
|
1574
|
-
|
|
1575
|
-
The component uses Shadow DOM for style encapsulation, but exposes CSS custom properties (CSS variables) that you can override to customize the appearance.
|
|
1576
|
-
|
|
1577
|
-
### Sizing
|
|
1578
|
-
|
|
1579
|
-
The component uses `--ms-rem` as a base unit for proportional scaling. Default is `10px`, meaning `calc(1.4 * var(--ms-rem))` equals `14px`.
|
|
1580
|
-
|
|
1581
|
-
**Global Scaling:**
|
|
1582
|
-
```html
|
|
1583
|
-
<!-- Compact (80%) -->
|
|
1584
|
-
<web-multiselect style="--ms-rem: 8px;"></web-multiselect>
|
|
1585
|
-
|
|
1586
|
-
<!-- Default (100%) -->
|
|
1587
|
-
<web-multiselect></web-multiselect>
|
|
1588
|
-
|
|
1589
|
-
<!-- Large (120%) -->
|
|
1590
|
-
<web-multiselect style="--ms-rem: 12px;"></web-multiselect>
|
|
1591
|
-
|
|
1592
|
-
<!-- Pure Admin integration (inherits from html { font-size: 10px }) -->
|
|
1593
|
-
<web-multiselect style="--ms-rem: 1rem;"></web-multiselect>
|
|
1594
|
-
```
|
|
1595
|
-
|
|
1596
|
-
**Via CSS class:**
|
|
1597
|
-
```css
|
|
1598
|
-
web-multiselect.compact { --ms-rem: 8px; }
|
|
1599
|
-
web-multiselect.large { --ms-rem: 12px; }
|
|
1600
|
-
```
|
|
1601
|
-
|
|
1602
|
-
**Shadow DOM Note:** CSS variables must be set on the `<web-multiselect>` element itself, not on wrapper divs.
|
|
1603
|
-
|
|
1604
|
-
**Fine-grained Control:**
|
|
1605
|
-
Override individual sizing variables for specific adjustments:
|
|
1606
|
-
- `--ms-input-height` - Input field height (default: 35px)
|
|
1607
|
-
- `--ms-input-font-size` - Input font size
|
|
1608
|
-
- `--ms-input-padding` - Input padding
|
|
1609
|
-
- `--ms-badge-height` - Badge height
|
|
1610
|
-
- `--ms-option-height` - Option height in dropdown
|
|
1611
|
-
|
|
1612
|
-
**Input Size Variants:**
|
|
1613
|
-
Five size variants for consistent input sizing across KeenMate components:
|
|
1614
|
-
|
|
1615
|
-
| Size | Variable | Height | Base Variable |
|
|
1616
|
-
|------|----------|--------|---------------|
|
|
1617
|
-
| XS | `--ms-input-size-xs-height` | 31px | `--base-input-size-xs-height` |
|
|
1618
|
-
| SM | `--ms-input-size-sm-height` | 33px | `--base-input-size-sm-height` |
|
|
1619
|
-
| MD | `--ms-input-size-md-height` | 35px | `--base-input-size-md-height` |
|
|
1620
|
-
| LG | `--ms-input-size-lg-height` | 38px | `--base-input-size-lg-height` |
|
|
1621
|
-
| XL | `--ms-input-size-xl-height` | 41px | `--base-input-size-xl-height` |
|
|
1622
|
-
|
|
1623
|
-
Heights reference `--base-input-size-*-height` from the [Theme Designer](https://theme-designer.keenmate.dev), ensuring consistent input heights across all KeenMate components.
|
|
1624
|
-
|
|
1625
|
-
```css
|
|
1626
|
-
/* Set consistent input heights across all components */
|
|
1627
|
-
:root {
|
|
1628
|
-
--base-input-size-md-height: 4.0; /* All components: 40px at 10px rem */
|
|
1629
|
-
}
|
|
1630
|
-
```
|
|
1631
|
-
|
|
1632
|
-
### Theme Designer
|
|
1633
|
-
|
|
1634
|
-
The easiest way to customize the appearance of this component is using the **KeenMate Theme Designer** at:
|
|
1635
|
-
|
|
1636
|
-
**[theme-designer.keenmate.dev](https://theme-designer.keenmate.dev)**
|
|
1637
|
-
|
|
1638
|
-
#### How It Works
|
|
1639
|
-
|
|
1640
|
-
1. **Choose 3 base colors** - background, text, and accent
|
|
1641
|
-
2. **Preview changes live** - see your theme applied instantly
|
|
1642
|
-
3. **Fine-tune individual variables** - lock specific values while adjusting others
|
|
1643
|
-
4. **Export your theme** - copy CSS, JSON, or SCSS to your project
|
|
1644
|
-
|
|
1645
|
-
#### CSS Variable Layers
|
|
1646
|
-
|
|
1647
|
-
KeenMate components support a **two-layer theming architecture**:
|
|
1648
|
-
|
|
1649
|
-
**Standalone Mode (Simple)** - Just override the component-specific variables you need:
|
|
1650
|
-
|
|
1651
|
-
```css
|
|
1652
|
-
:root {
|
|
1653
|
-
--ms-accent-color: #your-brand-color;
|
|
1654
|
-
--ms-primary-bg: #your-background;
|
|
1655
|
-
--ms-text-primary: #your-text-color;
|
|
1656
|
-
}
|
|
1657
|
-
```
|
|
1658
|
-
|
|
1659
|
-
**Cascading Mode (Multi-Component)** - When using multiple KeenMate components, you can define a shared base layer:
|
|
1660
|
-
|
|
1661
|
-
```css
|
|
1662
|
-
:root {
|
|
1663
|
-
/* Base layer - single source of truth */
|
|
1664
|
-
--base-accent-color: #3b82f6;
|
|
1665
|
-
--base-main-bg: #ffffff;
|
|
1666
|
-
--base-hover-bg: #f3f4f6;
|
|
1667
|
-
--base-text-color-1: #111827;
|
|
1668
|
-
|
|
1669
|
-
/* Components reference base layer */
|
|
1670
|
-
--ms-accent-color: var(--base-accent-color);
|
|
1671
|
-
--drp-accent-color: var(--base-accent-color);
|
|
1672
|
-
}
|
|
1673
|
-
```
|
|
1674
|
-
|
|
1675
|
-
Change `--base-accent-color` once → all components update automatically.
|
|
1676
|
-
|
|
1677
|
-
#### Unified Variable Naming
|
|
1678
|
-
|
|
1679
|
-
All KeenMate components follow a consistent naming convention for **Tier 1 variables** (core theming):
|
|
1680
|
-
|
|
1681
|
-
| Purpose | web-multiselect | web-daterangepicker |
|
|
1682
|
-
|---------|-----------------|---------------------|
|
|
1683
|
-
| Brand color | `--ms-accent-color` | `--drp-accent-color` |
|
|
1684
|
-
| Background | `--ms-primary-bg` | `--drp-primary-bg` |
|
|
1685
|
-
| Text color | `--ms-text-primary` | `--drp-text-primary` |
|
|
1686
|
-
| Text on accent | `--ms-text-color-on-accent` | `--drp-text-on-accent` |
|
|
1687
|
-
| Border color | `--ms-border-color` | `--drp-border-color` |
|
|
1688
|
-
|
|
1689
|
-
Learn the pattern once, apply it across all components.
|
|
1690
|
-
|
|
1691
|
-
#### Component Variables Manifest
|
|
1692
|
-
|
|
1693
|
-
This package exports a `component-variables.manifest.json` file that documents all supported CSS variables for tooling integration (e.g., Theme Designer, IDE autocomplete):
|
|
1694
|
-
|
|
1695
|
-
```javascript
|
|
1696
|
-
import manifest from '@keenmate/web-multiselect/component-variables.manifest.json';
|
|
1697
|
-
// manifest.baseVariables - list of --base-* variables the component responds to
|
|
1698
|
-
// manifest.componentVariables - list of --ms-* component-specific variables
|
|
1699
|
-
```
|
|
1700
|
-
|
|
1701
|
-
### CSS Variables (No Build System Required)
|
|
1702
|
-
|
|
1703
|
-
You can customize the component using CSS variables even with just a `<script>` tag:
|
|
1704
|
-
|
|
1705
|
-
```html
|
|
1706
|
-
<style>
|
|
1707
|
-
/* Override tooltip appearance */
|
|
1708
|
-
web-multiselect {
|
|
1709
|
-
--ms-tooltip-bg: #1f2937;
|
|
1710
|
-
--ms-tooltip-color: #f9fafb;
|
|
1711
|
-
--ms-tooltip-padding: 0.625rem 0.875rem;
|
|
1712
|
-
--ms-tooltip-border-radius: 0.5rem;
|
|
1713
|
-
--ms-tooltip-font-size: 0.8125rem;
|
|
1714
|
-
--ms-tooltip-max-width: 24rem;
|
|
1715
|
-
--ms-tooltip-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
|
1716
|
-
--ms-tooltip-z-index: 10000;
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
/* Override "+X more" badge colors */
|
|
1720
|
-
web-multiselect {
|
|
1721
|
-
--ms-more-badge-bg: #dbeafe;
|
|
1722
|
-
--ms-more-badge-hover-bg: #bfdbfe;
|
|
1723
|
-
--ms-more-badge-active-bg: #93c5fd;
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
/* Size the component */
|
|
1727
|
-
web-multiselect {
|
|
1728
|
-
width: 100%;
|
|
1729
|
-
max-width: 400px;
|
|
1730
|
-
}
|
|
1731
|
-
</style>
|
|
1732
|
-
```
|
|
1733
|
-
|
|
1734
|
-
### Dark mode — supported signals
|
|
1735
|
-
|
|
1736
|
-
Since v1.12.0 the multiselect honors **five different signals** for switching to dark mode. Pick whichever fits your app; you don't need to wire them all up.
|
|
1737
|
-
|
|
1738
|
-
| # | Signal | Set by | Example |
|
|
1739
|
-
|---|--------|--------|---------|
|
|
1740
|
-
| 1 | OS preference + page `color-scheme` | App author | `html { color-scheme: light dark }` — multiselect picks the OS branch automatically. |
|
|
1741
|
-
| 2 | Page-level `color-scheme: dark` | App author | `body { color-scheme: dark }` — flips every multiselect on the page to dark. |
|
|
1742
|
-
| 3 | Framework data-attribute on ancestor | Bootstrap, Pure Admin, custom apps | `<html data-bs-theme="dark">` or `<div data-theme="dark">…</div>` |
|
|
1743
|
-
| 4 | Framework class on ancestor | Tailwind, hand-rolled toggles | `<html class="dark">` |
|
|
1744
|
-
| 5 | Per-instance attribute on host | App author, for one widget | `<web-multiselect data-theme="dark">` |
|
|
1745
|
-
|
|
1746
|
-
**Precedence** (highest wins): per-instance (#5) → framework ancestor (#3, #4) → page color-scheme (#1, #2).
|
|
1747
|
-
|
|
1748
|
-
#### Forcing a single widget back to light
|
|
1749
|
-
|
|
1750
|
-
If your page is dark but you want one multiselect to render light:
|
|
1751
|
-
|
|
1752
|
-
```html
|
|
1753
|
-
<!-- on a body { color-scheme: dark } page -->
|
|
1754
|
-
<web-multiselect data-theme="light"></web-multiselect>
|
|
1755
|
-
```
|
|
1756
|
-
|
|
1757
|
-
This works for any of signals #3–#5. The symmetric `data-theme="light"`, `data-bs-theme="light"`, `.light` selectors restore the light palette inside the affected scope.
|
|
1758
|
-
|
|
1759
|
-
### Available CSS Variables
|
|
1760
|
-
|
|
1761
|
-
The component exposes **150+ CSS custom properties** defined at the `:host` level, making them inspectable and overridable. Below are the **50+ most commonly customized variables** organized by category.
|
|
1762
|
-
|
|
1763
|
-
#### Inspecting Variables in DevTools
|
|
1764
|
-
|
|
1765
|
-
All CSS custom properties are now defined at the `:host` level in the compiled CSS, making them visible in browser DevTools:
|
|
1766
|
-
|
|
1767
|
-
1. Open DevTools (F12) and select the `<web-multiselect>` element
|
|
1768
|
-
2. In the **Styles** panel, look for the `:host` selector
|
|
1769
|
-
3. You'll see all 150+ variables with their default values
|
|
1770
|
-
4. Edit values live to preview changes instantly
|
|
1771
|
-
|
|
1772
|
-
**CSS variables work with Shadow DOM** because they inherit through the shadow boundary. This means you can customize the component from outside:
|
|
1773
|
-
|
|
1774
|
-
```html
|
|
1775
|
-
<style>
|
|
1776
|
-
/* These variables will penetrate into the Shadow DOM */
|
|
1777
|
-
web-multiselect {
|
|
1778
|
-
--ms-accent-color: #10b981; /* Changes primary color */
|
|
1779
|
-
--ms-input-border-radius: 0.5rem; /* Rounds input corners */
|
|
1780
|
-
}
|
|
1781
|
-
</style>
|
|
1782
|
-
```
|
|
1783
|
-
|
|
1784
|
-
For the complete list of all available CSS variables, see:
|
|
1785
|
-
- [variables.css](./src/css/variables.css) - All 150+ CSS custom properties at `:host` level
|
|
1786
|
-
|
|
1787
|
-
#### Colors
|
|
1788
|
-
|
|
1789
|
-
| Variable | Default | Description |
|
|
1790
|
-
|----------|---------|-------------|
|
|
1791
|
-
| `--ms-accent-color` | `#3b82f6` | Primary accent color (blue) |
|
|
1792
|
-
| `--ms-accent-color-hover` | `#2563eb` | Accent color on hover |
|
|
1793
|
-
| `--ms-accent-color-active` | `#1d4ed8` | Accent color when active |
|
|
1794
|
-
| `--ms-text-primary` | `#111827` | Primary text color |
|
|
1795
|
-
| `--ms-text-secondary` | `#6b7280` | Secondary/muted text color |
|
|
1796
|
-
| `--ms-border-color` | `#e5e7eb` | Default border color |
|
|
1797
|
-
| `--ms-border` | `var(--base-border, 1px solid var(--ms-border-color))` | Default full border (inherits from theme-designer) |
|
|
1798
|
-
|
|
1799
|
-
#### Input Component
|
|
1800
|
-
|
|
1801
|
-
| Variable | Default | Description |
|
|
1802
|
-
|----------|---------|-------------|
|
|
1803
|
-
| `--ms-input-bg` | `var(--base-input-bg, #ffffff)` | Input background |
|
|
1804
|
-
| `--ms-input-text` | `#111827` | Input text color |
|
|
1805
|
-
| `--ms-input-border` | `#d1d5db` | Input border color |
|
|
1806
|
-
| `--ms-input-focus-border-color` | `#3b82f6` | Border color when focused |
|
|
1807
|
-
| `--ms-input-padding-v` | `0.5rem` | Input vertical padding |
|
|
1808
|
-
| `--ms-input-padding-h` | `0.75rem` | Input horizontal padding |
|
|
1809
|
-
| `--ms-input-font-size` | `0.875rem` | Input font size |
|
|
1810
|
-
| `--ms-input-border-radius` | `0.375rem` | Input border radius |
|
|
1811
|
-
| `--ms-input-placeholder-color` | `#6b7280` | Placeholder text color |
|
|
1812
|
-
|
|
1813
|
-
#### Dropdown & Options
|
|
1814
|
-
|
|
1815
|
-
| Variable | Default | Description |
|
|
1816
|
-
|----------|---------|-------------|
|
|
1817
|
-
| `--ms-dropdown-bg` | `var(--base-dropdown-bg, var(--base-elevated-bg, light-dark(#ffffff, #1a1a1a)))` | Dropdown background (auto-adapts to OS dark mode) |
|
|
1818
|
-
| `--ms-dropdown-border` | `var(--ms-border-color)` | Dropdown border color |
|
|
1819
|
-
| `--ms-dropdown-shadow` | (box shadow) | Dropdown shadow |
|
|
1820
|
-
| `--ms-dropdown-max-height` | `20rem` | Max height of dropdown |
|
|
1821
|
-
| `--ms-option-padding-v` | `0.5rem` | Option vertical padding |
|
|
1822
|
-
| `--ms-option-padding-h` | `0.75rem` | Option horizontal padding |
|
|
1823
|
-
| `--ms-option-hover-bg` | `#f9fafb` | Option background on hover |
|
|
1824
|
-
| `--ms-option-color-hover` | `inherit` | Option text color on hover |
|
|
1825
|
-
| `--ms-option-bg-selected` | (rgba accent) | Selected option background |
|
|
1826
|
-
| `--ms-option-bg-focused` | `#f9fafb` | Focused option background (keyboard) |
|
|
1827
|
-
| `--ms-option-color-focused` | `inherit` | Focused option text color |
|
|
1828
|
-
| `--ms-option-bg-matched` | (accent 8%) | Matched option background (navigate mode) |
|
|
1829
|
-
| `--ms-option-color-matched` | `inherit` | Matched option text color |
|
|
1830
|
-
|
|
1831
|
-
#### Badges
|
|
1832
|
-
|
|
1833
|
-
| Variable | Default | Description |
|
|
1834
|
-
|----------|---------|-------------|
|
|
1835
|
-
| `--ms-badge-text-bg` | `var(--ms-accent-color-light)` | Badge background color |
|
|
1836
|
-
| `--ms-badge-text-color` | `var(--ms-accent-color)` | Badge text color |
|
|
1837
|
-
| `--ms-badge-gap` | `0.5rem` | Gap between badges |
|
|
1838
|
-
| `--ms-badge-height` | `1.5rem` | Height of badges |
|
|
1839
|
-
| `--ms-badge-font-size` | `0.75rem` | Badge font size |
|
|
1840
|
-
| `--ms-badge-border-radius` | `0.375rem` | Badge border radius |
|
|
1841
|
-
| `--ms-badge-remove-bg` | `var(--ms-accent-color)` | Remove button background |
|
|
1842
|
-
| `--ms-badge-remove-color` | `var(--ms-text-color-on-accent)` | Remove button (X) color — applied to the SVG via `currentColor` |
|
|
1843
|
-
| `--ms-badge-remove-icon-size` | `calc(1.0 * var(--ms-rem))` | Size of the X glyph inside the remove button |
|
|
1844
|
-
| `--ms-icon-remove` | (inline SVG `url(...)`) | The X mask SVG; override to swap the glyph shape (alpha-only — color comes from `--ms-badge-remove-color`) |
|
|
1845
|
-
| `--ms-badge-counter-text-bg` | `var(--ms-primary-bg)` | BadgeCounter text background ("+X more") |
|
|
1846
|
-
| `--ms-badge-counter-text-color` | `var(--ms-text-color-3)` | BadgeCounter text color |
|
|
1847
|
-
| `--ms-badge-counter-remove-bg` | `var(--ms-text-color-3)` | BadgeCounter remove button background |
|
|
1848
|
-
| `--ms-badge-counter-remove-color` | `var(--ms-text-color-on-accent)` | BadgeCounter remove button color |
|
|
1849
|
-
| `--ms-badge-counter-border` | `1px solid var(--ms-border-color)` | BadgeCounter border |
|
|
1850
|
-
| `--ms-badge-text-border` | `none` | Badge text border (e.g., `1px solid #3b82f6`) |
|
|
1851
|
-
|
|
1852
|
-
#### Checkboxes
|
|
1853
|
-
|
|
1854
|
-
| Variable | Default | Description |
|
|
1855
|
-
|----------|---------|-------------|
|
|
1856
|
-
| `--ms-checkbox-bg` | `var(--ms-input-bg)` | Checkbox background |
|
|
1857
|
-
| `--ms-checkbox-border` | `1px solid var(--ms-border-color)` | Checkbox border |
|
|
1858
|
-
| `--ms-checkbox-border-radius` | `0.3rem` | Checkbox border radius |
|
|
1859
|
-
| `--ms-checkbox-checked-bg` | `var(--ms-accent-color)` | Background when checked |
|
|
1860
|
-
| `--ms-checkbox-checked-border` | `1px solid var(--ms-accent-color)` | Border when checked |
|
|
1861
|
-
| `--ms-checkbox-checkmark-color` | `var(--ms-text-color-on-accent)` | Checkmark color |
|
|
1862
|
-
| `--ms-checkbox-hover-border-color` | `var(--ms-accent-color)` | Border on hover |
|
|
1863
|
-
| `--ms-checkbox-disabled-bg` | `var(--ms-primary-bg)` | Disabled background |
|
|
1864
|
-
| `--ms-checkbox-disabled-border` | `1px solid var(--ms-border-color)` | Disabled border |
|
|
1865
|
-
|
|
1866
|
-
#### Scrollbar
|
|
1867
|
-
|
|
1868
|
-
| Variable | Default | Description |
|
|
1869
|
-
|----------|---------|-------------|
|
|
1870
|
-
| `--ms-scrollbar-width` | `8px` | Scrollbar width |
|
|
1871
|
-
| `--ms-scrollbar-track-bg` | `transparent` | Track background |
|
|
1872
|
-
| `--ms-scrollbar-thumb-bg` | `var(--ms-border-color)` | Thumb color |
|
|
1873
|
-
| `--ms-scrollbar-thumb-bg-hover` | `var(--ms-text-color-3)` | Thumb hover color |
|
|
1874
|
-
| `--ms-scrollbar-thumb-border-radius` | `4px` | Thumb border radius |
|
|
1875
|
-
|
|
1876
|
-
#### Counter (in input)
|
|
1877
|
-
|
|
1878
|
-
| Variable | Default | Description |
|
|
1879
|
-
|----------|---------|-------------|
|
|
1880
|
-
| `--ms-counter-bg` | `#3b82f6` | Counter background |
|
|
1881
|
-
| `--ms-counter-color` | `#ffffff` | Counter text color |
|
|
1882
|
-
| `--ms-counter-font-size` | `0.75rem` | Counter font size |
|
|
1883
|
-
| `--ms-counter-bg-hover` | `#2563eb` | Hover background color |
|
|
1884
|
-
|
|
1885
|
-
#### Tooltips
|
|
1886
|
-
|
|
1887
|
-
| Variable | Default | Description |
|
|
1888
|
-
|----------|---------|-------------|
|
|
1889
|
-
| `--ms-tooltip-bg` | `var(--base-tooltip-bg, var(--base-inverse-bg, light-dark(#333333, #f5f5f5)))` | Tooltip background (auto-adapts to OS dark mode) |
|
|
1890
|
-
| `--ms-tooltip-color` | `var(--ms-tooltip-text-color)` | Tooltip text color |
|
|
1891
|
-
| `--ms-tooltip-padding` | `0.5rem 0.75rem` | Tooltip padding |
|
|
1892
|
-
| `--ms-tooltip-border-radius` | `0.375rem` | Tooltip border radius |
|
|
1893
|
-
| `--ms-tooltip-font-size` | `0.875rem` | Tooltip font size |
|
|
1894
|
-
| `--ms-tooltip-max-width` | `20rem` | Tooltip maximum width |
|
|
1895
|
-
| `--ms-tooltip-shadow` | (box shadow) | Tooltip box shadow |
|
|
1896
|
-
| `--ms-tooltip-z-index` | `10000` | Tooltip z-index |
|
|
1897
|
-
|
|
1898
|
-
#### Typography
|
|
1899
|
-
|
|
1900
|
-
| Variable | Default | Description |
|
|
1901
|
-
|----------|---------|-------------|
|
|
1902
|
-
| `--ms-font-size-xs` | `0.75rem` | Extra small font size |
|
|
1903
|
-
| `--ms-font-size-sm` | `0.875rem` | Small font size |
|
|
1904
|
-
| `--ms-font-size-base` | `1rem` | Base font size |
|
|
1905
|
-
| `--ms-font-weight-medium` | `500` | Medium font weight |
|
|
1906
|
-
| `--ms-font-weight-semibold` | `600` | Semibold font weight |
|
|
1907
|
-
|
|
1908
|
-
#### Effects & Transitions
|
|
1909
|
-
|
|
1910
|
-
| Variable | Default | Description |
|
|
1911
|
-
|----------|---------|-------------|
|
|
1912
|
-
| `--ms-transition-fast` | `150ms` | Fast transition duration |
|
|
1913
|
-
| `--ms-transition-normal` | `200ms` | Normal transition duration |
|
|
1914
|
-
| `--ms-easing-snappy` | (cubic-bezier) | Snappy easing function |
|
|
1915
|
-
| `--ms-shadow-md` | (box shadow) | Medium shadow |
|
|
1916
|
-
| `--ms-shadow-xl` | (box shadow) | Extra large shadow |
|
|
1917
|
-
| `--ms-disabled-opacity` | `0.5` | Opacity for disabled state |
|
|
1918
|
-
|
|
1919
|
-
### Cascade layers / override contract
|
|
1920
|
-
|
|
1921
|
-
Since v1.12.0, the component's internal CSS is organized into named `@layer`s:
|
|
1922
|
-
|
|
1923
|
-
```css
|
|
1924
|
-
@layer variables, component, overrides;
|
|
1925
|
-
```
|
|
1926
|
-
|
|
1927
|
-
This gives consumers a predictable escape hatch when they need to override a rule from outside the shadow DOM (e.g. via `web-multiselect ::part(...)` or descendant selectors that reach into composed light DOM):
|
|
1928
|
-
|
|
1929
|
-
| Where your rule lives | Wins against |
|
|
1930
|
-
|---|---|
|
|
1931
|
-
| Unlayered consumer rule | Every internal layer (no `!important` needed) |
|
|
1932
|
-
| Consumer `@layer overrides` block | Component's `overrides` layer if loaded later in the stylesheet stack |
|
|
1933
|
-
| `:root { --base-* }` declaration | Component's `variables` layer trivially |
|
|
1934
|
-
| `web-multiselect { --ms-* }` element selector | Same as above, with higher specificity |
|
|
1935
|
-
|
|
1936
|
-
In practice you rarely need to think about layers — variables-first theming (`--ms-*` and `--base-*` overrides) covers ~95% of customization. Layers exist for the residual 5% where you need to flip a property the variable system doesn't expose.
|
|
1937
|
-
|
|
1938
|
-
### Advanced: Direct CSS Import
|
|
1939
|
-
|
|
1940
|
-
For users who want to import the raw CSS source files:
|
|
1941
|
-
|
|
1942
|
-
```css
|
|
1943
|
-
/* Import component CSS directly */
|
|
1944
|
-
@import '@keenmate/web-multiselect/css';
|
|
1945
|
-
|
|
1946
|
-
/* Or import individual partials */
|
|
1947
|
-
@import '@keenmate/web-multiselect/src/css/variables.css';
|
|
1948
|
-
@import '@keenmate/web-multiselect/src/css/base.css';
|
|
1949
|
-
/* ... etc */
|
|
1950
|
-
```
|
|
1951
|
-
|
|
1952
|
-
## Browser Support
|
|
1953
|
-
|
|
1954
|
-
- Modern browsers with Web Components support
|
|
1955
|
-
- Chrome/Edge 67+
|
|
1956
|
-
- Firefox 63+
|
|
1957
|
-
- Safari 10.1+
|
|
83
|
+
See [docs/usage.md](./docs/usage.md) for the full API and [docs/examples.md](./docs/examples.md) for advanced patterns (async data, virtual scrolling, custom rendering, form integration).
|
|
1958
84
|
|
|
1959
|
-
##
|
|
85
|
+
## Browser support
|
|
1960
86
|
|
|
1961
|
-
|
|
87
|
+
Modern evergreen browsers — anything with native `customElements`, Shadow DOM, and CSS `@layer` support:
|
|
1962
88
|
|
|
1963
|
-
|
|
89
|
+
- Chrome / Edge 99+
|
|
90
|
+
- Firefox 97+
|
|
91
|
+
- Safari 15.4+
|
|
1964
92
|
|
|
1965
|
-
|
|
1966
|
-
- The component will not render during SSR
|
|
1967
|
-
- It will only become interactive after hydration in the browser
|
|
1968
|
-
- No special client-side import wrappers are required
|
|
93
|
+
No polyfills are shipped. SSR-safe: the module imports without crashing in Node, but renders only after hydration in the browser.
|
|
1969
94
|
|
|
1970
95
|
## Development
|
|
1971
96
|
|
|
@@ -1973,28 +98,23 @@ This is a **client-side only** web component that uses Shadow DOM and browser AP
|
|
|
1973
98
|
# Install dependencies
|
|
1974
99
|
npm install
|
|
1975
100
|
|
|
1976
|
-
# Start dev server
|
|
101
|
+
# Start dev server (HMR)
|
|
1977
102
|
npm run dev
|
|
1978
103
|
|
|
1979
104
|
# Build for production
|
|
1980
105
|
npm run build
|
|
1981
106
|
|
|
1982
|
-
# Create package
|
|
107
|
+
# Create package tarball
|
|
1983
108
|
npm run package
|
|
1984
109
|
```
|
|
1985
110
|
|
|
1986
111
|
## License
|
|
1987
112
|
|
|
1988
|
-
|
|
113
|
+
MIT — see [LICENSE](./LICENSE).
|
|
1989
114
|
|
|
1990
|
-
|
|
115
|
+
## Built with BlissFramework
|
|
1991
116
|
|
|
1992
|
-
|
|
1993
|
-
- ✅ Free to use in commercial products
|
|
1994
|
-
- ✅ Free to modify and distribute
|
|
1995
|
-
- ✅ No licensing fees or restrictions
|
|
1996
|
-
- ⚠️ Provided "as is" without warranty
|
|
1997
|
-
- 📝 Must include copyright notice in copies
|
|
117
|
+
Follows the [BlissFramework component guidelines](https://blissframework.dev/) for structure, theming, color-scheme, and accessibility. Per-check verifications run via `/validate-web-component`.
|
|
1998
118
|
|
|
1999
119
|
## Credits
|
|
2000
120
|
|